@bytebase/dbhub 0.13.1 → 0.14.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.
- package/README.md +13 -27
- package/dist/{chunk-KBVJEDZF.js → chunk-WGDSRFBW.js} +449 -183
- package/dist/index.js +126 -83
- package/dist/{registry-AWAIN6WO.js → registry-FVGT25UH.js} +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -32,12 +32,11 @@
|
|
|
32
32
|
|
|
33
33
|
DBHub is a Minimal Database MCP Server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
|
|
34
34
|
|
|
35
|
-
- **
|
|
36
|
-
- **Multi-Database
|
|
35
|
+
- **Token Efficient**: Just two general MCP tools (execute_sql, search_objects) to minimize context window usage, plus support for custom tools
|
|
36
|
+
- **Multi-Database**: Single interface for PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite
|
|
37
37
|
- **Secure Access**: Read-only mode, SSH tunneling, and SSL/TLS encryption support
|
|
38
38
|
- **Multiple Connections**: Connect to multiple databases simultaneously with TOML configuration
|
|
39
39
|
- **Production-Ready**: Row limiting, lock timeout control, and connection pooling
|
|
40
|
-
- **MCP Native**: Full implementation of Model Context Protocol with comprehensive tools
|
|
41
40
|
|
|
42
41
|
## Supported Databases
|
|
43
42
|
|
|
@@ -72,13 +71,13 @@ docker run --rm --init \
|
|
|
72
71
|
**NPM:**
|
|
73
72
|
|
|
74
73
|
```bash
|
|
75
|
-
npx @bytebase/dbhub --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
74
|
+
npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
76
75
|
```
|
|
77
76
|
|
|
78
77
|
**Demo Mode:**
|
|
79
78
|
|
|
80
79
|
```bash
|
|
81
|
-
npx @bytebase/dbhub --transport http --port 8080 --demo
|
|
80
|
+
npx @bytebase/dbhub@latest --transport http --port 8080 --demo
|
|
82
81
|
```
|
|
83
82
|
|
|
84
83
|
See [Server Options](https://dbhub.ai/config/server-options) for all available parameters.
|
|
@@ -91,31 +90,18 @@ See [Multi-Database Configuration](https://dbhub.ai/config/multi-database) for c
|
|
|
91
90
|
|
|
92
91
|
## Development
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
pnpm install
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
1. Run in development mode:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
pnpm dev
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
1. Build for production:
|
|
107
|
-
```bash
|
|
108
|
-
pnpm build
|
|
109
|
-
pnpm start --transport stdio --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### Testing
|
|
93
|
+
```bash
|
|
94
|
+
# Install dependencies
|
|
95
|
+
pnpm install
|
|
113
96
|
|
|
114
|
-
|
|
97
|
+
# Run in development mode
|
|
98
|
+
pnpm dev
|
|
115
99
|
|
|
116
|
-
|
|
100
|
+
# Build and run for production
|
|
101
|
+
pnpm build && pnpm start --transport stdio --dsn "postgres://user:password@localhost:5432/dbname"
|
|
102
|
+
```
|
|
117
103
|
|
|
118
|
-
See [Debug](https://dbhub.ai/config/debug).
|
|
104
|
+
See [Testing](.claude/skills/testing/SKILL.md) and [Debug](https://dbhub.ai/config/debug).
|
|
119
105
|
|
|
120
106
|
## Contributors
|
|
121
107
|
|
|
@@ -62,161 +62,11 @@ var ConnectorRegistry = _ConnectorRegistry;
|
|
|
62
62
|
|
|
63
63
|
// src/utils/ssh-tunnel.ts
|
|
64
64
|
import { Client } from "ssh2";
|
|
65
|
-
import { readFileSync } from "fs";
|
|
65
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
66
66
|
import { createServer } from "net";
|
|
67
|
-
var SSHTunnel = class {
|
|
68
|
-
constructor() {
|
|
69
|
-
this.sshClient = null;
|
|
70
|
-
this.localServer = null;
|
|
71
|
-
this.tunnelInfo = null;
|
|
72
|
-
this.isConnected = false;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Establish an SSH tunnel
|
|
76
|
-
* @param config SSH connection configuration
|
|
77
|
-
* @param options Tunnel options including target host and port
|
|
78
|
-
* @returns Promise resolving to tunnel information including local port
|
|
79
|
-
*/
|
|
80
|
-
async establish(config, options) {
|
|
81
|
-
if (this.isConnected) {
|
|
82
|
-
throw new Error("SSH tunnel is already established");
|
|
83
|
-
}
|
|
84
|
-
return new Promise((resolve, reject) => {
|
|
85
|
-
this.sshClient = new Client();
|
|
86
|
-
const sshConfig = {
|
|
87
|
-
host: config.host,
|
|
88
|
-
port: config.port || 22,
|
|
89
|
-
username: config.username
|
|
90
|
-
};
|
|
91
|
-
if (config.password) {
|
|
92
|
-
sshConfig.password = config.password;
|
|
93
|
-
} else if (config.privateKey) {
|
|
94
|
-
try {
|
|
95
|
-
const privateKey = readFileSync(config.privateKey);
|
|
96
|
-
sshConfig.privateKey = privateKey;
|
|
97
|
-
if (config.passphrase) {
|
|
98
|
-
sshConfig.passphrase = config.passphrase;
|
|
99
|
-
}
|
|
100
|
-
} catch (error) {
|
|
101
|
-
reject(new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`));
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
reject(new Error("Either password or privateKey must be provided for SSH authentication"));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
this.sshClient.on("error", (err) => {
|
|
109
|
-
this.cleanup();
|
|
110
|
-
reject(new Error(`SSH connection error: ${err.message}`));
|
|
111
|
-
});
|
|
112
|
-
this.sshClient.on("ready", () => {
|
|
113
|
-
console.error("SSH connection established");
|
|
114
|
-
this.localServer = createServer((localSocket) => {
|
|
115
|
-
this.sshClient.forwardOut(
|
|
116
|
-
"127.0.0.1",
|
|
117
|
-
0,
|
|
118
|
-
options.targetHost,
|
|
119
|
-
options.targetPort,
|
|
120
|
-
(err, stream) => {
|
|
121
|
-
if (err) {
|
|
122
|
-
console.error("SSH forward error:", err);
|
|
123
|
-
localSocket.end();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
localSocket.pipe(stream).pipe(localSocket);
|
|
127
|
-
stream.on("error", (err2) => {
|
|
128
|
-
console.error("SSH stream error:", err2);
|
|
129
|
-
localSocket.end();
|
|
130
|
-
});
|
|
131
|
-
localSocket.on("error", (err2) => {
|
|
132
|
-
console.error("Local socket error:", err2);
|
|
133
|
-
stream.end();
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
);
|
|
137
|
-
});
|
|
138
|
-
const localPort = options.localPort || 0;
|
|
139
|
-
this.localServer.listen(localPort, "127.0.0.1", () => {
|
|
140
|
-
const address = this.localServer.address();
|
|
141
|
-
if (!address || typeof address === "string") {
|
|
142
|
-
this.cleanup();
|
|
143
|
-
reject(new Error("Failed to get local server address"));
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
this.tunnelInfo = {
|
|
147
|
-
localPort: address.port,
|
|
148
|
-
targetHost: options.targetHost,
|
|
149
|
-
targetPort: options.targetPort
|
|
150
|
-
};
|
|
151
|
-
this.isConnected = true;
|
|
152
|
-
console.error(`SSH tunnel established: localhost:${address.port} -> ${options.targetHost}:${options.targetPort}`);
|
|
153
|
-
resolve(this.tunnelInfo);
|
|
154
|
-
});
|
|
155
|
-
this.localServer.on("error", (err) => {
|
|
156
|
-
this.cleanup();
|
|
157
|
-
reject(new Error(`Local server error: ${err.message}`));
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
this.sshClient.connect(sshConfig);
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Close the SSH tunnel and clean up resources
|
|
165
|
-
*/
|
|
166
|
-
async close() {
|
|
167
|
-
if (!this.isConnected) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
return new Promise((resolve) => {
|
|
171
|
-
this.cleanup();
|
|
172
|
-
this.isConnected = false;
|
|
173
|
-
console.error("SSH tunnel closed");
|
|
174
|
-
resolve();
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Clean up resources
|
|
179
|
-
*/
|
|
180
|
-
cleanup() {
|
|
181
|
-
if (this.localServer) {
|
|
182
|
-
this.localServer.close();
|
|
183
|
-
this.localServer = null;
|
|
184
|
-
}
|
|
185
|
-
if (this.sshClient) {
|
|
186
|
-
this.sshClient.end();
|
|
187
|
-
this.sshClient = null;
|
|
188
|
-
}
|
|
189
|
-
this.tunnelInfo = null;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Get current tunnel information
|
|
193
|
-
*/
|
|
194
|
-
getTunnelInfo() {
|
|
195
|
-
return this.tunnelInfo;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Check if tunnel is connected
|
|
199
|
-
*/
|
|
200
|
-
getIsConnected() {
|
|
201
|
-
return this.isConnected;
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// src/config/toml-loader.ts
|
|
206
|
-
import fs2 from "fs";
|
|
207
|
-
import path2 from "path";
|
|
208
|
-
import { homedir as homedir3 } from "os";
|
|
209
|
-
import toml from "@iarna/toml";
|
|
210
|
-
|
|
211
|
-
// src/config/env.ts
|
|
212
|
-
import dotenv from "dotenv";
|
|
213
|
-
import path from "path";
|
|
214
|
-
import fs from "fs";
|
|
215
|
-
import { fileURLToPath } from "url";
|
|
216
|
-
import { homedir as homedir2 } from "os";
|
|
217
67
|
|
|
218
68
|
// src/utils/ssh-config-parser.ts
|
|
219
|
-
import { readFileSync
|
|
69
|
+
import { readFileSync, realpathSync, statSync } from "fs";
|
|
220
70
|
import { homedir } from "os";
|
|
221
71
|
import { join } from "path";
|
|
222
72
|
import SSHConfig from "ssh-config";
|
|
@@ -232,28 +82,38 @@ function expandTilde(filePath) {
|
|
|
232
82
|
}
|
|
233
83
|
return filePath;
|
|
234
84
|
}
|
|
235
|
-
function
|
|
85
|
+
function resolveSymlink(filePath) {
|
|
86
|
+
const expandedPath = expandTilde(filePath);
|
|
236
87
|
try {
|
|
237
|
-
return
|
|
88
|
+
return realpathSync(expandedPath);
|
|
89
|
+
} catch {
|
|
90
|
+
return expandedPath;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function isFile(filePath) {
|
|
94
|
+
try {
|
|
95
|
+
const stat = statSync(filePath);
|
|
96
|
+
return stat.isFile();
|
|
238
97
|
} catch {
|
|
239
98
|
return false;
|
|
240
99
|
}
|
|
241
100
|
}
|
|
242
101
|
function findDefaultSSHKey() {
|
|
243
102
|
for (const keyPath of DEFAULT_SSH_KEYS) {
|
|
244
|
-
|
|
245
|
-
|
|
103
|
+
const resolvedPath = resolveSymlink(keyPath);
|
|
104
|
+
if (isFile(resolvedPath)) {
|
|
105
|
+
return resolvedPath;
|
|
246
106
|
}
|
|
247
107
|
}
|
|
248
108
|
return void 0;
|
|
249
109
|
}
|
|
250
110
|
function parseSSHConfig(hostAlias, configPath) {
|
|
251
|
-
const sshConfigPath = configPath;
|
|
252
|
-
if (!
|
|
111
|
+
const sshConfigPath = resolveSymlink(configPath);
|
|
112
|
+
if (!isFile(sshConfigPath)) {
|
|
253
113
|
return null;
|
|
254
114
|
}
|
|
255
115
|
try {
|
|
256
|
-
const configContent =
|
|
116
|
+
const configContent = readFileSync(sshConfigPath, "utf8");
|
|
257
117
|
const config = SSHConfig.parse(configContent);
|
|
258
118
|
const hostConfig = config.compute(hostAlias);
|
|
259
119
|
if (!hostConfig || !hostConfig.HostName && !hostConfig.User) {
|
|
@@ -273,9 +133,9 @@ function parseSSHConfig(hostAlias, configPath) {
|
|
|
273
133
|
}
|
|
274
134
|
if (hostConfig.IdentityFile) {
|
|
275
135
|
const identityFile = Array.isArray(hostConfig.IdentityFile) ? hostConfig.IdentityFile[0] : hostConfig.IdentityFile;
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
sshConfig.privateKey =
|
|
136
|
+
const resolvedPath = resolveSymlink(identityFile);
|
|
137
|
+
if (isFile(resolvedPath)) {
|
|
138
|
+
sshConfig.privateKey = resolvedPath;
|
|
279
139
|
}
|
|
280
140
|
}
|
|
281
141
|
if (!sshConfig.privateKey) {
|
|
@@ -284,8 +144,11 @@ function parseSSHConfig(hostAlias, configPath) {
|
|
|
284
144
|
sshConfig.privateKey = defaultKey;
|
|
285
145
|
}
|
|
286
146
|
}
|
|
287
|
-
if (hostConfig.ProxyJump
|
|
288
|
-
|
|
147
|
+
if (hostConfig.ProxyJump) {
|
|
148
|
+
sshConfig.proxyJump = hostConfig.ProxyJump;
|
|
149
|
+
}
|
|
150
|
+
if (hostConfig.ProxyCommand) {
|
|
151
|
+
console.error("Warning: ProxyCommand in SSH config is not supported by DBHub. Use ProxyJump instead.");
|
|
289
152
|
}
|
|
290
153
|
if (!sshConfig.host || !sshConfig.username) {
|
|
291
154
|
return null;
|
|
@@ -308,6 +171,332 @@ function looksLikeSSHAlias(host) {
|
|
|
308
171
|
}
|
|
309
172
|
return true;
|
|
310
173
|
}
|
|
174
|
+
function validatePort(port, jumpHostStr) {
|
|
175
|
+
if (isNaN(port) || port <= 0 || port > 65535) {
|
|
176
|
+
throw new Error(`Invalid port number in "${jumpHostStr}": port must be between 1 and 65535`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function parseJumpHost(jumpHostStr) {
|
|
180
|
+
let username;
|
|
181
|
+
let host;
|
|
182
|
+
let port = 22;
|
|
183
|
+
let remaining = jumpHostStr.trim();
|
|
184
|
+
if (!remaining) {
|
|
185
|
+
throw new Error("Jump host string cannot be empty");
|
|
186
|
+
}
|
|
187
|
+
const atIndex = remaining.indexOf("@");
|
|
188
|
+
if (atIndex !== -1) {
|
|
189
|
+
const extractedUsername = remaining.substring(0, atIndex).trim();
|
|
190
|
+
if (extractedUsername) {
|
|
191
|
+
username = extractedUsername;
|
|
192
|
+
}
|
|
193
|
+
remaining = remaining.substring(atIndex + 1);
|
|
194
|
+
}
|
|
195
|
+
if (remaining.startsWith("[")) {
|
|
196
|
+
const closeBracket = remaining.indexOf("]");
|
|
197
|
+
if (closeBracket !== -1) {
|
|
198
|
+
host = remaining.substring(1, closeBracket);
|
|
199
|
+
const afterBracket = remaining.substring(closeBracket + 1);
|
|
200
|
+
if (afterBracket.startsWith(":")) {
|
|
201
|
+
const parsedPort = parseInt(afterBracket.substring(1), 10);
|
|
202
|
+
validatePort(parsedPort, jumpHostStr);
|
|
203
|
+
port = parsedPort;
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
throw new Error(`Invalid ProxyJump host "${jumpHostStr}": missing closing bracket in IPv6 address`);
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
const lastColon = remaining.lastIndexOf(":");
|
|
210
|
+
if (lastColon !== -1) {
|
|
211
|
+
const potentialPort = remaining.substring(lastColon + 1);
|
|
212
|
+
if (/^\d+$/.test(potentialPort)) {
|
|
213
|
+
host = remaining.substring(0, lastColon);
|
|
214
|
+
const parsedPort = parseInt(potentialPort, 10);
|
|
215
|
+
validatePort(parsedPort, jumpHostStr);
|
|
216
|
+
port = parsedPort;
|
|
217
|
+
} else {
|
|
218
|
+
host = remaining;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
host = remaining;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!host) {
|
|
225
|
+
throw new Error(`Invalid jump host format: "${jumpHostStr}" - host cannot be empty`);
|
|
226
|
+
}
|
|
227
|
+
return { host, port, username };
|
|
228
|
+
}
|
|
229
|
+
function parseJumpHosts(proxyJump) {
|
|
230
|
+
if (!proxyJump || proxyJump.trim() === "" || proxyJump.toLowerCase() === "none") {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
return proxyJump.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map(parseJumpHost);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/utils/ssh-tunnel.ts
|
|
237
|
+
var SSHTunnel = class {
|
|
238
|
+
constructor() {
|
|
239
|
+
this.sshClients = [];
|
|
240
|
+
// All SSH clients in the chain
|
|
241
|
+
this.localServer = null;
|
|
242
|
+
this.tunnelInfo = null;
|
|
243
|
+
this.isConnected = false;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Establish an SSH tunnel, optionally through jump hosts (ProxyJump).
|
|
247
|
+
* @param config SSH connection configuration
|
|
248
|
+
* @param options Tunnel options including target host and port
|
|
249
|
+
* @returns Promise resolving to tunnel information including local port
|
|
250
|
+
*/
|
|
251
|
+
async establish(config, options) {
|
|
252
|
+
if (this.isConnected) {
|
|
253
|
+
throw new Error("SSH tunnel is already established");
|
|
254
|
+
}
|
|
255
|
+
this.isConnected = true;
|
|
256
|
+
try {
|
|
257
|
+
const jumpHosts = config.proxyJump ? parseJumpHosts(config.proxyJump) : [];
|
|
258
|
+
let privateKeyBuffer;
|
|
259
|
+
if (config.privateKey) {
|
|
260
|
+
try {
|
|
261
|
+
const resolvedKeyPath = resolveSymlink(config.privateKey);
|
|
262
|
+
privateKeyBuffer = readFileSync2(resolvedKeyPath);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
throw new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (!config.password && !privateKeyBuffer) {
|
|
268
|
+
throw new Error("Either password or privateKey must be provided for SSH authentication");
|
|
269
|
+
}
|
|
270
|
+
const finalClient = await this.establishChain(jumpHosts, config, privateKeyBuffer);
|
|
271
|
+
return await this.createLocalTunnel(finalClient, options);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
this.cleanup();
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Establish a chain of SSH connections through jump hosts.
|
|
279
|
+
* @returns The final SSH client connected to the target host
|
|
280
|
+
*/
|
|
281
|
+
async establishChain(jumpHosts, targetConfig, privateKey) {
|
|
282
|
+
let previousStream;
|
|
283
|
+
for (let i = 0; i < jumpHosts.length; i++) {
|
|
284
|
+
const jumpHost = jumpHosts[i];
|
|
285
|
+
const nextHost = i + 1 < jumpHosts.length ? jumpHosts[i + 1] : { host: targetConfig.host, port: targetConfig.port || 22 };
|
|
286
|
+
let client = null;
|
|
287
|
+
let forwardStream;
|
|
288
|
+
try {
|
|
289
|
+
client = await this.connectToHost(
|
|
290
|
+
{
|
|
291
|
+
host: jumpHost.host,
|
|
292
|
+
port: jumpHost.port,
|
|
293
|
+
username: jumpHost.username || targetConfig.username
|
|
294
|
+
},
|
|
295
|
+
targetConfig.password,
|
|
296
|
+
privateKey,
|
|
297
|
+
targetConfig.passphrase,
|
|
298
|
+
previousStream,
|
|
299
|
+
`jump host ${i + 1}`
|
|
300
|
+
);
|
|
301
|
+
console.error(` \u2192 Forwarding through ${jumpHost.host}:${jumpHost.port} to ${nextHost.host}:${nextHost.port}`);
|
|
302
|
+
forwardStream = await this.forwardTo(client, nextHost.host, nextHost.port);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (client) {
|
|
305
|
+
try {
|
|
306
|
+
client.end();
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
this.sshClients.push(client);
|
|
313
|
+
previousStream = forwardStream;
|
|
314
|
+
}
|
|
315
|
+
const finalClient = await this.connectToHost(
|
|
316
|
+
{
|
|
317
|
+
host: targetConfig.host,
|
|
318
|
+
port: targetConfig.port || 22,
|
|
319
|
+
username: targetConfig.username
|
|
320
|
+
},
|
|
321
|
+
targetConfig.password,
|
|
322
|
+
privateKey,
|
|
323
|
+
targetConfig.passphrase,
|
|
324
|
+
previousStream,
|
|
325
|
+
jumpHosts.length > 0 ? "target host" : void 0
|
|
326
|
+
);
|
|
327
|
+
this.sshClients.push(finalClient);
|
|
328
|
+
return finalClient;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Connect to a single SSH host.
|
|
332
|
+
*/
|
|
333
|
+
connectToHost(hostInfo, password, privateKey, passphrase, sock, label) {
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
const client = new Client();
|
|
336
|
+
const sshConfig = {
|
|
337
|
+
host: hostInfo.host,
|
|
338
|
+
port: hostInfo.port,
|
|
339
|
+
username: hostInfo.username
|
|
340
|
+
};
|
|
341
|
+
if (password) {
|
|
342
|
+
sshConfig.password = password;
|
|
343
|
+
}
|
|
344
|
+
if (privateKey) {
|
|
345
|
+
sshConfig.privateKey = privateKey;
|
|
346
|
+
if (passphrase) {
|
|
347
|
+
sshConfig.passphrase = passphrase;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (sock) {
|
|
351
|
+
sshConfig.sock = sock;
|
|
352
|
+
}
|
|
353
|
+
const onError = (err) => {
|
|
354
|
+
client.removeListener("ready", onReady);
|
|
355
|
+
client.destroy();
|
|
356
|
+
reject(new Error(`SSH connection error${label ? ` (${label})` : ""}: ${err.message}`));
|
|
357
|
+
};
|
|
358
|
+
const onReady = () => {
|
|
359
|
+
client.removeListener("error", onError);
|
|
360
|
+
const desc = label || `${hostInfo.host}:${hostInfo.port}`;
|
|
361
|
+
console.error(`SSH connection established: ${desc}`);
|
|
362
|
+
resolve(client);
|
|
363
|
+
};
|
|
364
|
+
client.on("error", onError);
|
|
365
|
+
client.on("ready", onReady);
|
|
366
|
+
client.connect(sshConfig);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Forward a connection through an SSH client to a target host.
|
|
371
|
+
*/
|
|
372
|
+
forwardTo(client, targetHost, targetPort) {
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
client.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream) => {
|
|
375
|
+
if (err) {
|
|
376
|
+
reject(new Error(`SSH forward error: ${err.message}`));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
resolve(stream);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Create the local server that tunnels connections to the database.
|
|
385
|
+
*/
|
|
386
|
+
createLocalTunnel(sshClient, options) {
|
|
387
|
+
return new Promise((resolve, reject) => {
|
|
388
|
+
let settled = false;
|
|
389
|
+
this.localServer = createServer((localSocket) => {
|
|
390
|
+
sshClient.forwardOut(
|
|
391
|
+
"127.0.0.1",
|
|
392
|
+
0,
|
|
393
|
+
options.targetHost,
|
|
394
|
+
options.targetPort,
|
|
395
|
+
(err, stream) => {
|
|
396
|
+
if (err) {
|
|
397
|
+
console.error("SSH forward error:", err);
|
|
398
|
+
localSocket.end();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
localSocket.pipe(stream).pipe(localSocket);
|
|
402
|
+
stream.on("error", (err2) => {
|
|
403
|
+
console.error("SSH stream error:", err2);
|
|
404
|
+
localSocket.end();
|
|
405
|
+
});
|
|
406
|
+
localSocket.on("error", (err2) => {
|
|
407
|
+
console.error("Local socket error:", err2);
|
|
408
|
+
stream.end();
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
this.localServer.on("error", (err) => {
|
|
414
|
+
if (!settled) {
|
|
415
|
+
settled = true;
|
|
416
|
+
reject(new Error(`Local server error: ${err.message}`));
|
|
417
|
+
} else {
|
|
418
|
+
console.error("Local server error after tunnel established:", err);
|
|
419
|
+
this.cleanup();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
const localPort = options.localPort || 0;
|
|
423
|
+
this.localServer.listen(localPort, "127.0.0.1", () => {
|
|
424
|
+
const address = this.localServer.address();
|
|
425
|
+
if (!address || typeof address === "string") {
|
|
426
|
+
if (!settled) {
|
|
427
|
+
settled = true;
|
|
428
|
+
reject(new Error("Failed to get local server address"));
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.tunnelInfo = {
|
|
433
|
+
localPort: address.port,
|
|
434
|
+
targetHost: options.targetHost,
|
|
435
|
+
targetPort: options.targetPort
|
|
436
|
+
};
|
|
437
|
+
console.error(`SSH tunnel established: localhost:${address.port} \u2192 ${options.targetHost}:${options.targetPort}`);
|
|
438
|
+
settled = true;
|
|
439
|
+
resolve(this.tunnelInfo);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Close the SSH tunnel and clean up resources
|
|
445
|
+
*/
|
|
446
|
+
async close() {
|
|
447
|
+
if (!this.isConnected) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
return new Promise((resolve) => {
|
|
451
|
+
this.cleanup();
|
|
452
|
+
console.error("SSH tunnel closed");
|
|
453
|
+
resolve();
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Clean up resources. Closes all SSH clients in reverse order (innermost first).
|
|
458
|
+
*/
|
|
459
|
+
cleanup() {
|
|
460
|
+
if (this.localServer) {
|
|
461
|
+
this.localServer.close();
|
|
462
|
+
this.localServer = null;
|
|
463
|
+
}
|
|
464
|
+
for (let i = this.sshClients.length - 1; i >= 0; i--) {
|
|
465
|
+
try {
|
|
466
|
+
this.sshClients[i].end();
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
this.sshClients = [];
|
|
471
|
+
this.tunnelInfo = null;
|
|
472
|
+
this.isConnected = false;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get current tunnel information
|
|
476
|
+
*/
|
|
477
|
+
getTunnelInfo() {
|
|
478
|
+
return this.tunnelInfo;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Check if tunnel is connected
|
|
482
|
+
*/
|
|
483
|
+
getIsConnected() {
|
|
484
|
+
return this.isConnected;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/config/toml-loader.ts
|
|
489
|
+
import fs2 from "fs";
|
|
490
|
+
import path2 from "path";
|
|
491
|
+
import { homedir as homedir3 } from "os";
|
|
492
|
+
import toml from "@iarna/toml";
|
|
493
|
+
|
|
494
|
+
// src/config/env.ts
|
|
495
|
+
import dotenv from "dotenv";
|
|
496
|
+
import path from "path";
|
|
497
|
+
import fs from "fs";
|
|
498
|
+
import { fileURLToPath } from "url";
|
|
499
|
+
import { homedir as homedir2 } from "os";
|
|
311
500
|
|
|
312
501
|
// src/utils/safe-url.ts
|
|
313
502
|
var SafeURL = class {
|
|
@@ -815,6 +1004,13 @@ function resolveSSHConfig() {
|
|
|
815
1004
|
config.passphrase = process.env.SSH_PASSPHRASE;
|
|
816
1005
|
sources.push("SSH_PASSPHRASE from environment");
|
|
817
1006
|
}
|
|
1007
|
+
if (args["ssh-proxy-jump"]) {
|
|
1008
|
+
config.proxyJump = args["ssh-proxy-jump"];
|
|
1009
|
+
sources.push("ssh-proxy-jump from command line");
|
|
1010
|
+
} else if (process.env.SSH_PROXY_JUMP) {
|
|
1011
|
+
config.proxyJump = process.env.SSH_PROXY_JUMP;
|
|
1012
|
+
sources.push("SSH_PROXY_JUMP from environment");
|
|
1013
|
+
}
|
|
818
1014
|
if (!config.host || !config.username) {
|
|
819
1015
|
throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
|
|
820
1016
|
}
|
|
@@ -917,8 +1113,13 @@ function loadTomlConfig() {
|
|
|
917
1113
|
try {
|
|
918
1114
|
const fileContent = fs2.readFileSync(configPath, "utf-8");
|
|
919
1115
|
const parsedToml = toml.parse(fileContent);
|
|
920
|
-
|
|
1116
|
+
if (!Array.isArray(parsedToml.sources)) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`Configuration file ${configPath}: must contain a [[sources]] array. Use [[sources]] syntax for array of tables in TOML.`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
921
1121
|
const sources = processSourceConfigs(parsedToml.sources, configPath);
|
|
1122
|
+
validateTomlConfig({ ...parsedToml, sources }, configPath);
|
|
922
1123
|
return {
|
|
923
1124
|
sources,
|
|
924
1125
|
tools: parsedToml.tools,
|
|
@@ -960,11 +1161,6 @@ id = "my_db"
|
|
|
960
1161
|
dsn = "postgres://..."`
|
|
961
1162
|
);
|
|
962
1163
|
}
|
|
963
|
-
if (!Array.isArray(config.sources)) {
|
|
964
|
-
throw new Error(
|
|
965
|
-
`Configuration file ${configPath}: 'sources' must be an array. Use [[sources]] syntax for array of tables in TOML.`
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
1164
|
if (config.sources.length === 0) {
|
|
969
1165
|
throw new Error(
|
|
970
1166
|
`Configuration file ${configPath}: sources array cannot be empty. Please define at least one source with [[sources]].`
|
|
@@ -1081,10 +1277,10 @@ function validateSourceConfig(source, configPath) {
|
|
|
1081
1277
|
);
|
|
1082
1278
|
}
|
|
1083
1279
|
}
|
|
1084
|
-
if (source.
|
|
1085
|
-
if (typeof source.
|
|
1280
|
+
if (source.query_timeout !== void 0) {
|
|
1281
|
+
if (typeof source.query_timeout !== "number" || source.query_timeout <= 0) {
|
|
1086
1282
|
throw new Error(
|
|
1087
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid
|
|
1283
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid query_timeout. Must be a positive number (in seconds).`
|
|
1088
1284
|
);
|
|
1089
1285
|
}
|
|
1090
1286
|
}
|
|
@@ -1095,6 +1291,54 @@ function validateSourceConfig(source, configPath) {
|
|
|
1095
1291
|
);
|
|
1096
1292
|
}
|
|
1097
1293
|
}
|
|
1294
|
+
if (source.sslmode !== void 0) {
|
|
1295
|
+
if (source.type === "sqlite") {
|
|
1296
|
+
throw new Error(
|
|
1297
|
+
`Configuration file ${configPath}: source '${source.id}' has sslmode but SQLite does not support SSL. Remove the sslmode field for SQLite sources.`
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
const validSslModes = ["disable", "require"];
|
|
1301
|
+
if (!validSslModes.includes(source.sslmode)) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid sslmode '${source.sslmode}'. Valid values: ${validSslModes.join(", ")}`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (source.authentication !== void 0) {
|
|
1308
|
+
if (source.type !== "sqlserver") {
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
`Configuration file ${configPath}: source '${source.id}' has authentication but it is only supported for SQL Server.`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
const validAuthMethods = ["ntlm", "azure-active-directory-access-token"];
|
|
1314
|
+
if (!validAuthMethods.includes(source.authentication)) {
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid authentication '${source.authentication}'. Valid values: ${validAuthMethods.join(", ")}`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
if (source.authentication === "ntlm" && !source.domain) {
|
|
1320
|
+
throw new Error(
|
|
1321
|
+
`Configuration file ${configPath}: source '${source.id}' uses NTLM authentication but 'domain' is not specified.`
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (source.domain !== void 0) {
|
|
1326
|
+
if (source.type !== "sqlserver") {
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
`Configuration file ${configPath}: source '${source.id}' has domain but it is only supported for SQL Server.`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
if (source.authentication === void 0) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
`Configuration file ${configPath}: source '${source.id}' has domain but authentication is not set. Add authentication = "ntlm" to use Windows domain authentication.`
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (source.authentication !== "ntlm") {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
`Configuration file ${configPath}: source '${source.id}' has domain but authentication is set to '${source.authentication}'. Domain is only valid with authentication = "ntlm".`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1098
1342
|
}
|
|
1099
1343
|
function processSourceConfigs(sources, configPath) {
|
|
1100
1344
|
return sources.map((source) => {
|
|
@@ -1154,9 +1398,15 @@ function buildDSNFromSource(source) {
|
|
|
1154
1398
|
}
|
|
1155
1399
|
return `sqlite:///${source.database}`;
|
|
1156
1400
|
}
|
|
1157
|
-
|
|
1401
|
+
const passwordRequired = source.authentication !== "azure-active-directory-access-token";
|
|
1402
|
+
if (!source.host || !source.user || !source.database) {
|
|
1158
1403
|
throw new Error(
|
|
1159
|
-
`Source '${source.id}': missing required connection parameters. Required: type, host, user,
|
|
1404
|
+
`Source '${source.id}': missing required connection parameters. Required: type, host, user, database`
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
if (passwordRequired && !source.password) {
|
|
1408
|
+
throw new Error(
|
|
1409
|
+
`Source '${source.id}': password is required. (Password is optional only for azure-active-directory-access-token authentication)`
|
|
1160
1410
|
);
|
|
1161
1411
|
}
|
|
1162
1412
|
const port = source.port || getDefaultPortForType(source.type);
|
|
@@ -1164,11 +1414,26 @@ function buildDSNFromSource(source) {
|
|
|
1164
1414
|
throw new Error(`Source '${source.id}': unable to determine port`);
|
|
1165
1415
|
}
|
|
1166
1416
|
const encodedUser = encodeURIComponent(source.user);
|
|
1167
|
-
const encodedPassword = encodeURIComponent(source.password);
|
|
1417
|
+
const encodedPassword = source.password ? encodeURIComponent(source.password) : "";
|
|
1168
1418
|
const encodedDatabase = encodeURIComponent(source.database);
|
|
1169
1419
|
let dsn = `${source.type}://${encodedUser}:${encodedPassword}@${source.host}:${port}/${encodedDatabase}`;
|
|
1170
|
-
|
|
1171
|
-
|
|
1420
|
+
const queryParams = [];
|
|
1421
|
+
if (source.type === "sqlserver") {
|
|
1422
|
+
if (source.instanceName) {
|
|
1423
|
+
queryParams.push(`instanceName=${encodeURIComponent(source.instanceName)}`);
|
|
1424
|
+
}
|
|
1425
|
+
if (source.authentication) {
|
|
1426
|
+
queryParams.push(`authentication=${encodeURIComponent(source.authentication)}`);
|
|
1427
|
+
}
|
|
1428
|
+
if (source.domain) {
|
|
1429
|
+
queryParams.push(`domain=${encodeURIComponent(source.domain)}`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
if (source.sslmode && source.type !== "sqlite") {
|
|
1433
|
+
queryParams.push(`sslmode=${source.sslmode}`);
|
|
1434
|
+
}
|
|
1435
|
+
if (queryParams.length > 0) {
|
|
1436
|
+
dsn += `?${queryParams.join("&")}`;
|
|
1172
1437
|
}
|
|
1173
1438
|
return dsn;
|
|
1174
1439
|
}
|
|
@@ -1197,6 +1462,7 @@ var ConnectorManager = class {
|
|
|
1197
1462
|
if (sources.length === 0) {
|
|
1198
1463
|
throw new Error("No sources provided");
|
|
1199
1464
|
}
|
|
1465
|
+
console.error(`Connecting to ${sources.length} database source(s)...`);
|
|
1200
1466
|
for (const source of sources) {
|
|
1201
1467
|
await this.connectSource(source);
|
|
1202
1468
|
}
|
|
@@ -1207,6 +1473,7 @@ var ConnectorManager = class {
|
|
|
1207
1473
|
async connectSource(source) {
|
|
1208
1474
|
const sourceId = source.id;
|
|
1209
1475
|
const dsn = buildDSNFromSource(source);
|
|
1476
|
+
console.error(` - ${sourceId}: ${redactDSN(dsn)}`);
|
|
1210
1477
|
let actualDSN = dsn;
|
|
1211
1478
|
if (source.ssh_host) {
|
|
1212
1479
|
if (!source.ssh_user) {
|
|
@@ -1220,7 +1487,8 @@ var ConnectorManager = class {
|
|
|
1220
1487
|
username: source.ssh_user,
|
|
1221
1488
|
password: source.ssh_password,
|
|
1222
1489
|
privateKey: source.ssh_key,
|
|
1223
|
-
passphrase: source.ssh_passphrase
|
|
1490
|
+
passphrase: source.ssh_passphrase,
|
|
1491
|
+
proxyJump: source.ssh_proxy_jump
|
|
1224
1492
|
};
|
|
1225
1493
|
if (!sshConfig.password && !sshConfig.privateKey) {
|
|
1226
1494
|
throw new Error(
|
|
@@ -1255,8 +1523,8 @@ var ConnectorManager = class {
|
|
|
1255
1523
|
if (source.connection_timeout !== void 0) {
|
|
1256
1524
|
config.connectionTimeoutSeconds = source.connection_timeout;
|
|
1257
1525
|
}
|
|
1258
|
-
if (
|
|
1259
|
-
config.
|
|
1526
|
+
if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
|
|
1527
|
+
config.queryTimeoutSeconds = source.query_timeout;
|
|
1260
1528
|
}
|
|
1261
1529
|
if (source.readonly !== void 0) {
|
|
1262
1530
|
config.readonly = source.readonly;
|
|
@@ -1812,11 +2080,9 @@ export {
|
|
|
1812
2080
|
isDemoMode,
|
|
1813
2081
|
resolveTransport,
|
|
1814
2082
|
resolvePort,
|
|
1815
|
-
redactDSN,
|
|
1816
2083
|
resolveSourceConfigs,
|
|
1817
2084
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
1818
2085
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
1819
|
-
buildDSNFromSource,
|
|
1820
2086
|
ConnectorManager,
|
|
1821
2087
|
mapArgumentsToArray,
|
|
1822
2088
|
ToolRegistry,
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
ConnectorManager,
|
|
6
6
|
ConnectorRegistry,
|
|
7
7
|
SafeURL,
|
|
8
|
-
buildDSNFromSource,
|
|
9
8
|
getDatabaseTypeFromDSN,
|
|
10
9
|
getDefaultPortForType,
|
|
11
10
|
getToolRegistry,
|
|
@@ -13,12 +12,11 @@ import {
|
|
|
13
12
|
mapArgumentsToArray,
|
|
14
13
|
obfuscateDSNPassword,
|
|
15
14
|
parseConnectionInfoFromDSN,
|
|
16
|
-
redactDSN,
|
|
17
15
|
resolvePort,
|
|
18
16
|
resolveSourceConfigs,
|
|
19
17
|
resolveTransport,
|
|
20
18
|
stripCommentsAndStrings
|
|
21
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-WGDSRFBW.js";
|
|
22
20
|
|
|
23
21
|
// src/connectors/postgres/index.ts
|
|
24
22
|
import pg from "pg";
|
|
@@ -148,6 +146,7 @@ var { Pool } = pg;
|
|
|
148
146
|
var PostgresDSNParser = class {
|
|
149
147
|
async parse(dsn, config) {
|
|
150
148
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
149
|
+
const queryTimeoutSeconds = config?.queryTimeoutSeconds;
|
|
151
150
|
if (!this.isValidDSN(dsn)) {
|
|
152
151
|
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
153
152
|
const expectedFormat = this.getSampleDSN();
|
|
@@ -159,7 +158,7 @@ Expected: ${expectedFormat}`
|
|
|
159
158
|
}
|
|
160
159
|
try {
|
|
161
160
|
const url = new SafeURL(dsn);
|
|
162
|
-
const
|
|
161
|
+
const poolConfig = {
|
|
163
162
|
host: url.hostname,
|
|
164
163
|
port: url.port ? parseInt(url.port) : 5432,
|
|
165
164
|
database: url.pathname ? url.pathname.substring(1) : "",
|
|
@@ -170,18 +169,21 @@ Expected: ${expectedFormat}`
|
|
|
170
169
|
url.forEachSearchParam((value, key) => {
|
|
171
170
|
if (key === "sslmode") {
|
|
172
171
|
if (value === "disable") {
|
|
173
|
-
|
|
172
|
+
poolConfig.ssl = false;
|
|
174
173
|
} else if (value === "require") {
|
|
175
|
-
|
|
174
|
+
poolConfig.ssl = { rejectUnauthorized: false };
|
|
176
175
|
} else {
|
|
177
|
-
|
|
176
|
+
poolConfig.ssl = true;
|
|
178
177
|
}
|
|
179
178
|
}
|
|
180
179
|
});
|
|
181
180
|
if (connectionTimeoutSeconds !== void 0) {
|
|
182
|
-
|
|
181
|
+
poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
|
|
183
182
|
}
|
|
184
|
-
|
|
183
|
+
if (queryTimeoutSeconds !== void 0) {
|
|
184
|
+
poolConfig.query_timeout = queryTimeoutSeconds * 1e3;
|
|
185
|
+
}
|
|
186
|
+
return poolConfig;
|
|
185
187
|
} catch (error) {
|
|
186
188
|
throw new Error(
|
|
187
189
|
`Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -521,7 +523,7 @@ import { DefaultAzureCredential } from "@azure/identity";
|
|
|
521
523
|
var SQLServerDSNParser = class {
|
|
522
524
|
async parse(dsn, config) {
|
|
523
525
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
524
|
-
const
|
|
526
|
+
const queryTimeoutSeconds = config?.queryTimeoutSeconds;
|
|
525
527
|
if (!this.isValidDSN(dsn)) {
|
|
526
528
|
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
527
529
|
const expectedFormat = this.getSampleDSN();
|
|
@@ -541,8 +543,16 @@ Expected: ${expectedFormat}`
|
|
|
541
543
|
options.sslmode = value;
|
|
542
544
|
} else if (key === "instanceName") {
|
|
543
545
|
options.instanceName = value;
|
|
546
|
+
} else if (key === "domain") {
|
|
547
|
+
options.domain = value;
|
|
544
548
|
}
|
|
545
549
|
});
|
|
550
|
+
if (options.authentication === "ntlm" && !options.domain) {
|
|
551
|
+
throw new Error("NTLM authentication requires 'domain' parameter");
|
|
552
|
+
}
|
|
553
|
+
if (options.domain && options.authentication !== "ntlm") {
|
|
554
|
+
throw new Error("Parameter 'domain' requires 'authentication=ntlm'");
|
|
555
|
+
}
|
|
546
556
|
if (options.sslmode) {
|
|
547
557
|
if (options.sslmode === "disable") {
|
|
548
558
|
options.encrypt = false;
|
|
@@ -553,8 +563,6 @@ Expected: ${expectedFormat}`
|
|
|
553
563
|
}
|
|
554
564
|
}
|
|
555
565
|
const config2 = {
|
|
556
|
-
user: url.username,
|
|
557
|
-
password: url.password,
|
|
558
566
|
server: url.hostname,
|
|
559
567
|
port: url.port ? parseInt(url.port) : 1433,
|
|
560
568
|
// Default SQL Server port
|
|
@@ -567,27 +575,44 @@ Expected: ${expectedFormat}`
|
|
|
567
575
|
...connectionTimeoutSeconds !== void 0 && {
|
|
568
576
|
connectTimeout: connectionTimeoutSeconds * 1e3
|
|
569
577
|
},
|
|
570
|
-
...
|
|
571
|
-
requestTimeout:
|
|
578
|
+
...queryTimeoutSeconds !== void 0 && {
|
|
579
|
+
requestTimeout: queryTimeoutSeconds * 1e3
|
|
572
580
|
},
|
|
573
581
|
instanceName: options.instanceName
|
|
574
582
|
// Add named instance support
|
|
575
583
|
}
|
|
576
584
|
};
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
585
|
+
switch (options.authentication) {
|
|
586
|
+
case "azure-active-directory-access-token": {
|
|
587
|
+
try {
|
|
588
|
+
const credential = new DefaultAzureCredential();
|
|
589
|
+
const token = await credential.getToken("https://database.windows.net/");
|
|
590
|
+
config2.authentication = {
|
|
591
|
+
type: "azure-active-directory-access-token",
|
|
592
|
+
options: {
|
|
593
|
+
token: token.token
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
} catch (error) {
|
|
597
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
598
|
+
throw new Error(`Failed to get Azure AD token: ${errorMessage}`);
|
|
599
|
+
}
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
case "ntlm":
|
|
581
603
|
config2.authentication = {
|
|
582
|
-
type: "
|
|
604
|
+
type: "ntlm",
|
|
583
605
|
options: {
|
|
584
|
-
|
|
606
|
+
domain: options.domain,
|
|
607
|
+
userName: url.username,
|
|
608
|
+
password: url.password
|
|
585
609
|
}
|
|
586
610
|
};
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
611
|
+
break;
|
|
612
|
+
default:
|
|
613
|
+
config2.user = url.username;
|
|
614
|
+
config2.password = url.password;
|
|
615
|
+
break;
|
|
591
616
|
}
|
|
592
617
|
return config2;
|
|
593
618
|
} catch (error) {
|
|
@@ -1327,6 +1352,16 @@ Expected: ${expectedFormat}`
|
|
|
1327
1352
|
if (connectionTimeoutSeconds !== void 0) {
|
|
1328
1353
|
config2.connectTimeout = connectionTimeoutSeconds * 1e3;
|
|
1329
1354
|
}
|
|
1355
|
+
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
1356
|
+
config2.authPlugins = {
|
|
1357
|
+
mysql_clear_password: () => () => {
|
|
1358
|
+
return Buffer.from(url.password + "\0");
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
if (config2.ssl === void 0) {
|
|
1362
|
+
config2.ssl = { rejectUnauthorized: false };
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1330
1365
|
return config2;
|
|
1331
1366
|
} catch (error) {
|
|
1332
1367
|
throw new Error(
|
|
@@ -1364,6 +1399,9 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1364
1399
|
try {
|
|
1365
1400
|
const connectionOptions = await this.dsnParser.parse(dsn, config);
|
|
1366
1401
|
this.pool = mysql.createPool(connectionOptions);
|
|
1402
|
+
if (config?.queryTimeoutSeconds !== void 0) {
|
|
1403
|
+
this.queryTimeoutMs = config.queryTimeoutSeconds * 1e3;
|
|
1404
|
+
}
|
|
1367
1405
|
const [rows] = await this.pool.query("SELECT 1");
|
|
1368
1406
|
} catch (err) {
|
|
1369
1407
|
console.error("Failed to connect to MySQL database:", err);
|
|
@@ -1666,7 +1704,7 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1666
1704
|
let results;
|
|
1667
1705
|
if (parameters && parameters.length > 0) {
|
|
1668
1706
|
try {
|
|
1669
|
-
results = await conn.query(processedSQL, parameters);
|
|
1707
|
+
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs }, parameters);
|
|
1670
1708
|
} catch (error) {
|
|
1671
1709
|
console.error(`[MySQL executeSQL] ERROR: ${error.message}`);
|
|
1672
1710
|
console.error(`[MySQL executeSQL] SQL: ${processedSQL}`);
|
|
@@ -1674,7 +1712,7 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1674
1712
|
throw error;
|
|
1675
1713
|
}
|
|
1676
1714
|
} else {
|
|
1677
|
-
results = await conn.query(processedSQL);
|
|
1715
|
+
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs });
|
|
1678
1716
|
}
|
|
1679
1717
|
const [firstResult] = results;
|
|
1680
1718
|
const rows = parseQueryResults(firstResult);
|
|
@@ -1695,6 +1733,7 @@ import mariadb from "mariadb";
|
|
|
1695
1733
|
var MariadbDSNParser = class {
|
|
1696
1734
|
async parse(dsn, config) {
|
|
1697
1735
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
1736
|
+
const queryTimeoutSeconds = config?.queryTimeoutSeconds;
|
|
1698
1737
|
if (!this.isValidDSN(dsn)) {
|
|
1699
1738
|
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
1700
1739
|
const expectedFormat = this.getSampleDSN();
|
|
@@ -1706,7 +1745,7 @@ Expected: ${expectedFormat}`
|
|
|
1706
1745
|
}
|
|
1707
1746
|
try {
|
|
1708
1747
|
const url = new SafeURL(dsn);
|
|
1709
|
-
const
|
|
1748
|
+
const connectionConfig = {
|
|
1710
1749
|
host: url.hostname,
|
|
1711
1750
|
port: url.port ? parseInt(url.port) : 3306,
|
|
1712
1751
|
database: url.pathname ? url.pathname.substring(1) : "",
|
|
@@ -1717,20 +1756,28 @@ Expected: ${expectedFormat}`
|
|
|
1717
1756
|
// Enable native multi-statement support
|
|
1718
1757
|
...connectionTimeoutSeconds !== void 0 && {
|
|
1719
1758
|
connectTimeout: connectionTimeoutSeconds * 1e3
|
|
1759
|
+
},
|
|
1760
|
+
...queryTimeoutSeconds !== void 0 && {
|
|
1761
|
+
queryTimeout: queryTimeoutSeconds * 1e3
|
|
1720
1762
|
}
|
|
1721
1763
|
};
|
|
1722
1764
|
url.forEachSearchParam((value, key) => {
|
|
1723
1765
|
if (key === "sslmode") {
|
|
1724
1766
|
if (value === "disable") {
|
|
1725
|
-
|
|
1767
|
+
connectionConfig.ssl = void 0;
|
|
1726
1768
|
} else if (value === "require") {
|
|
1727
|
-
|
|
1769
|
+
connectionConfig.ssl = { rejectUnauthorized: false };
|
|
1728
1770
|
} else {
|
|
1729
|
-
|
|
1771
|
+
connectionConfig.ssl = {};
|
|
1730
1772
|
}
|
|
1731
1773
|
}
|
|
1732
1774
|
});
|
|
1733
|
-
|
|
1775
|
+
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
1776
|
+
if (connectionConfig.ssl === void 0) {
|
|
1777
|
+
connectionConfig.ssl = { rejectUnauthorized: false };
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return connectionConfig;
|
|
1734
1781
|
} catch (error) {
|
|
1735
1782
|
throw new Error(
|
|
1736
1783
|
`Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -2229,7 +2276,7 @@ function getClientIdentifier(extra) {
|
|
|
2229
2276
|
|
|
2230
2277
|
// src/tools/execute-sql.ts
|
|
2231
2278
|
var executeSqlSchema = {
|
|
2232
|
-
sql: z.string().describe("SQL
|
|
2279
|
+
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
2233
2280
|
};
|
|
2234
2281
|
function splitSQLStatements(sql2) {
|
|
2235
2282
|
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
@@ -2291,12 +2338,12 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
2291
2338
|
// src/tools/search-objects.ts
|
|
2292
2339
|
import { z as z2 } from "zod";
|
|
2293
2340
|
var searchDatabaseObjectsSchema = {
|
|
2294
|
-
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("
|
|
2295
|
-
pattern: z2.string().optional().default("%").describe("
|
|
2296
|
-
schema: z2.string().optional().describe("Filter
|
|
2297
|
-
table: z2.string().optional().describe("Filter to
|
|
2298
|
-
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("
|
|
2299
|
-
limit: z2.number().int().positive().max(1e3).default(100).describe("
|
|
2341
|
+
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Object type to search"),
|
|
2342
|
+
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
|
|
2343
|
+
schema: z2.string().optional().describe("Filter to schema"),
|
|
2344
|
+
table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
|
|
2345
|
+
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
|
|
2346
|
+
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results (default: 100, max: 1000)")
|
|
2300
2347
|
};
|
|
2301
2348
|
function likePatternToRegex(pattern) {
|
|
2302
2349
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
@@ -2686,12 +2733,12 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
2686
2733
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2687
2734
|
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
|
|
2688
2735
|
const dbType = sourceConfig.type;
|
|
2689
|
-
const
|
|
2690
|
-
const
|
|
2691
|
-
const title =
|
|
2736
|
+
const isSingleSource = sourceIds.length === 1;
|
|
2737
|
+
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
2738
|
+
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
2692
2739
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
2693
2740
|
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
2694
|
-
const description = `Execute SQL queries on the
|
|
2741
|
+
const description = isSingleSource ? `Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
2695
2742
|
const isReadonly = executeOptions.readonly === true;
|
|
2696
2743
|
const annotations = {
|
|
2697
2744
|
title,
|
|
@@ -2711,10 +2758,14 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
2711
2758
|
annotations
|
|
2712
2759
|
};
|
|
2713
2760
|
}
|
|
2714
|
-
function getSearchObjectsMetadata(sourceId
|
|
2715
|
-
const
|
|
2716
|
-
const
|
|
2717
|
-
const
|
|
2761
|
+
function getSearchObjectsMetadata(sourceId) {
|
|
2762
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2763
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2764
|
+
const dbType = sourceConfig.type;
|
|
2765
|
+
const isSingleSource = sourceIds.length === 1;
|
|
2766
|
+
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
2767
|
+
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
2768
|
+
const description = isSingleSource ? `Search and list database objects (schemas, tables, columns, procedures, indexes) on the ${dbType} database` : `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database`;
|
|
2718
2769
|
return {
|
|
2719
2770
|
name: toolName,
|
|
2720
2771
|
description,
|
|
@@ -2742,11 +2793,7 @@ function buildExecuteSqlTool(sourceId) {
|
|
|
2742
2793
|
};
|
|
2743
2794
|
}
|
|
2744
2795
|
function buildSearchObjectsTool(sourceId) {
|
|
2745
|
-
const
|
|
2746
|
-
const dbType = sourceConfig.type;
|
|
2747
|
-
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2748
|
-
const isDefault = sourceIds[0] === sourceId;
|
|
2749
|
-
const searchMetadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
|
|
2796
|
+
const searchMetadata = getSearchObjectsMetadata(sourceId);
|
|
2750
2797
|
return {
|
|
2751
2798
|
name: searchMetadata.name,
|
|
2752
2799
|
description: searchMetadata.description,
|
|
@@ -2755,31 +2802,37 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
2755
2802
|
name: "object_type",
|
|
2756
2803
|
type: "string",
|
|
2757
2804
|
required: true,
|
|
2758
|
-
description: "
|
|
2805
|
+
description: "Object type to search"
|
|
2759
2806
|
},
|
|
2760
2807
|
{
|
|
2761
2808
|
name: "pattern",
|
|
2762
2809
|
type: "string",
|
|
2763
2810
|
required: false,
|
|
2764
|
-
description: "
|
|
2811
|
+
description: "LIKE pattern (% = any chars, _ = one char). Default: %"
|
|
2765
2812
|
},
|
|
2766
2813
|
{
|
|
2767
2814
|
name: "schema",
|
|
2768
2815
|
type: "string",
|
|
2769
2816
|
required: false,
|
|
2770
|
-
description: "Filter
|
|
2817
|
+
description: "Filter to schema"
|
|
2818
|
+
},
|
|
2819
|
+
{
|
|
2820
|
+
name: "table",
|
|
2821
|
+
type: "string",
|
|
2822
|
+
required: false,
|
|
2823
|
+
description: "Filter to table (requires schema; column/index only)"
|
|
2771
2824
|
},
|
|
2772
2825
|
{
|
|
2773
2826
|
name: "detail_level",
|
|
2774
2827
|
type: "string",
|
|
2775
2828
|
required: false,
|
|
2776
|
-
description: "
|
|
2829
|
+
description: "Detail: names (minimal), summary (metadata), full (all)"
|
|
2777
2830
|
},
|
|
2778
2831
|
{
|
|
2779
2832
|
name: "limit",
|
|
2780
2833
|
type: "integer",
|
|
2781
2834
|
required: false,
|
|
2782
|
-
description: "
|
|
2835
|
+
description: "Max results (default: 100, max: 1000)"
|
|
2783
2836
|
}
|
|
2784
2837
|
]
|
|
2785
2838
|
};
|
|
@@ -2925,21 +2978,18 @@ function registerTools(server) {
|
|
|
2925
2978
|
const registry = getToolRegistry();
|
|
2926
2979
|
for (const sourceId of sourceIds) {
|
|
2927
2980
|
const enabledTools = registry.getEnabledToolConfigs(sourceId);
|
|
2928
|
-
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2929
|
-
const dbType = sourceConfig.type;
|
|
2930
|
-
const isDefault = sourceIds[0] === sourceId;
|
|
2931
2981
|
for (const toolConfig of enabledTools) {
|
|
2932
2982
|
if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
|
|
2933
|
-
registerExecuteSqlTool(server, sourceId
|
|
2983
|
+
registerExecuteSqlTool(server, sourceId);
|
|
2934
2984
|
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
2935
|
-
registerSearchObjectsTool(server, sourceId
|
|
2985
|
+
registerSearchObjectsTool(server, sourceId);
|
|
2936
2986
|
} else {
|
|
2937
|
-
registerCustomTool(server,
|
|
2987
|
+
registerCustomTool(server, sourceId, toolConfig);
|
|
2938
2988
|
}
|
|
2939
2989
|
}
|
|
2940
2990
|
}
|
|
2941
2991
|
}
|
|
2942
|
-
function registerExecuteSqlTool(server, sourceId
|
|
2992
|
+
function registerExecuteSqlTool(server, sourceId) {
|
|
2943
2993
|
const metadata = getExecuteSqlMetadata(sourceId);
|
|
2944
2994
|
server.registerTool(
|
|
2945
2995
|
metadata.name,
|
|
@@ -2951,8 +3001,8 @@ function registerExecuteSqlTool(server, sourceId, dbType) {
|
|
|
2951
3001
|
createExecuteSqlToolHandler(sourceId)
|
|
2952
3002
|
);
|
|
2953
3003
|
}
|
|
2954
|
-
function registerSearchObjectsTool(server, sourceId
|
|
2955
|
-
const metadata = getSearchObjectsMetadata(sourceId
|
|
3004
|
+
function registerSearchObjectsTool(server, sourceId) {
|
|
3005
|
+
const metadata = getSearchObjectsMetadata(sourceId);
|
|
2956
3006
|
server.registerTool(
|
|
2957
3007
|
metadata.name,
|
|
2958
3008
|
{
|
|
@@ -2969,7 +3019,9 @@ function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
|
|
|
2969
3019
|
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
2970
3020
|
);
|
|
2971
3021
|
}
|
|
2972
|
-
function registerCustomTool(server,
|
|
3022
|
+
function registerCustomTool(server, sourceId, toolConfig) {
|
|
3023
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
3024
|
+
const dbType = sourceConfig.type;
|
|
2973
3025
|
const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
|
|
2974
3026
|
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
2975
3027
|
server.registerTool(
|
|
@@ -2991,7 +3043,7 @@ function registerCustomTool(server, toolConfig, dbType) {
|
|
|
2991
3043
|
}
|
|
2992
3044
|
|
|
2993
3045
|
// src/api/sources.ts
|
|
2994
|
-
function transformSourceConfig(source
|
|
3046
|
+
function transformSourceConfig(source) {
|
|
2995
3047
|
if (!source.type && source.dsn) {
|
|
2996
3048
|
const inferredType = getDatabaseTypeFromDSN(source.dsn);
|
|
2997
3049
|
if (inferredType) {
|
|
@@ -3003,8 +3055,7 @@ function transformSourceConfig(source, isDefault) {
|
|
|
3003
3055
|
}
|
|
3004
3056
|
const dataSource = {
|
|
3005
3057
|
id: source.id,
|
|
3006
|
-
type: source.type
|
|
3007
|
-
is_default: isDefault
|
|
3058
|
+
type: source.type
|
|
3008
3059
|
};
|
|
3009
3060
|
if (source.host) {
|
|
3010
3061
|
dataSource.host = source.host;
|
|
@@ -3043,9 +3094,8 @@ function transformSourceConfig(source, isDefault) {
|
|
|
3043
3094
|
function listSources(req, res) {
|
|
3044
3095
|
try {
|
|
3045
3096
|
const sourceConfigs = ConnectorManager.getAllSourceConfigs();
|
|
3046
|
-
const sources = sourceConfigs.map((config
|
|
3047
|
-
|
|
3048
|
-
return transformSourceConfig(config, isDefault);
|
|
3097
|
+
const sources = sourceConfigs.map((config) => {
|
|
3098
|
+
return transformSourceConfig(config);
|
|
3049
3099
|
});
|
|
3050
3100
|
res.json(sources);
|
|
3051
3101
|
} catch (error) {
|
|
@@ -3059,7 +3109,6 @@ function listSources(req, res) {
|
|
|
3059
3109
|
function getSource(req, res) {
|
|
3060
3110
|
try {
|
|
3061
3111
|
const sourceId = req.params.sourceId;
|
|
3062
|
-
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
3063
3112
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
3064
3113
|
if (!sourceConfig) {
|
|
3065
3114
|
const errorResponse = {
|
|
@@ -3069,8 +3118,7 @@ function getSource(req, res) {
|
|
|
3069
3118
|
res.status(404).json(errorResponse);
|
|
3070
3119
|
return;
|
|
3071
3120
|
}
|
|
3072
|
-
const
|
|
3073
|
-
const dataSource = transformSourceConfig(sourceConfig, isDefault);
|
|
3121
|
+
const dataSource = transformSourceConfig(sourceConfig);
|
|
3074
3122
|
res.json(dataSource);
|
|
3075
3123
|
} catch (error) {
|
|
3076
3124
|
console.error(`Error getting source ${req.params.sourceId}:`, error);
|
|
@@ -3265,13 +3313,8 @@ See documentation for more details on configuring database connections.
|
|
|
3265
3313
|
const connectorManager = new ConnectorManager();
|
|
3266
3314
|
const sources = sourceConfigsData.sources;
|
|
3267
3315
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
3268
|
-
console.error(`Connecting to ${sources.length} database source(s)...`);
|
|
3269
|
-
for (const source of sources) {
|
|
3270
|
-
const dsn = source.dsn || buildDSNFromSource(source);
|
|
3271
|
-
console.error(` - ${source.id}: ${redactDSN(dsn)}`);
|
|
3272
|
-
}
|
|
3273
3316
|
await connectorManager.connectWithSources(sources);
|
|
3274
|
-
const { initializeToolRegistry } = await import("./registry-
|
|
3317
|
+
const { initializeToolRegistry } = await import("./registry-FVGT25UH.js");
|
|
3275
3318
|
initializeToolRegistry({
|
|
3276
3319
|
sources: sourceConfigsData.sources,
|
|
3277
3320
|
tools: sourceConfigsData.tools
|
|
@@ -3365,9 +3408,9 @@ See documentation for more details on configuring database connections.
|
|
|
3365
3408
|
console.error(" Backend API: http://localhost:8080");
|
|
3366
3409
|
console.error("");
|
|
3367
3410
|
} else {
|
|
3368
|
-
console.error(`Admin console at http://
|
|
3411
|
+
console.error(`Admin console at http://localhost:${port}/`);
|
|
3369
3412
|
}
|
|
3370
|
-
console.error(`MCP server endpoint at http://
|
|
3413
|
+
console.error(`MCP server endpoint at http://localhost:${port}/mcp`);
|
|
3371
3414
|
});
|
|
3372
3415
|
} else {
|
|
3373
3416
|
const server = createServer();
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"mcpName": "io.github.bytebase/dbhub",
|
|
5
|
-
"description": "Minimal Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
|
|
5
|
+
"description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/bytebase/dbhub.git"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@azure/identity": "^4.8.0",
|
|
40
40
|
"@iarna/toml": "^2.2.5",
|
|
41
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
42
42
|
"better-sqlite3": "^11.9.0",
|
|
43
43
|
"dotenv": "^16.4.7",
|
|
44
44
|
"express": "^4.18.2",
|