@bytebase/dbhub 0.13.2 → 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 +113 -85
- 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) {
|
|
@@ -1374,6 +1399,9 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1374
1399
|
try {
|
|
1375
1400
|
const connectionOptions = await this.dsnParser.parse(dsn, config);
|
|
1376
1401
|
this.pool = mysql.createPool(connectionOptions);
|
|
1402
|
+
if (config?.queryTimeoutSeconds !== void 0) {
|
|
1403
|
+
this.queryTimeoutMs = config.queryTimeoutSeconds * 1e3;
|
|
1404
|
+
}
|
|
1377
1405
|
const [rows] = await this.pool.query("SELECT 1");
|
|
1378
1406
|
} catch (err) {
|
|
1379
1407
|
console.error("Failed to connect to MySQL database:", err);
|
|
@@ -1676,7 +1704,7 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1676
1704
|
let results;
|
|
1677
1705
|
if (parameters && parameters.length > 0) {
|
|
1678
1706
|
try {
|
|
1679
|
-
results = await conn.query(processedSQL, parameters);
|
|
1707
|
+
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs }, parameters);
|
|
1680
1708
|
} catch (error) {
|
|
1681
1709
|
console.error(`[MySQL executeSQL] ERROR: ${error.message}`);
|
|
1682
1710
|
console.error(`[MySQL executeSQL] SQL: ${processedSQL}`);
|
|
@@ -1684,7 +1712,7 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1684
1712
|
throw error;
|
|
1685
1713
|
}
|
|
1686
1714
|
} else {
|
|
1687
|
-
results = await conn.query(processedSQL);
|
|
1715
|
+
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs });
|
|
1688
1716
|
}
|
|
1689
1717
|
const [firstResult] = results;
|
|
1690
1718
|
const rows = parseQueryResults(firstResult);
|
|
@@ -1705,6 +1733,7 @@ import mariadb from "mariadb";
|
|
|
1705
1733
|
var MariadbDSNParser = class {
|
|
1706
1734
|
async parse(dsn, config) {
|
|
1707
1735
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
1736
|
+
const queryTimeoutSeconds = config?.queryTimeoutSeconds;
|
|
1708
1737
|
if (!this.isValidDSN(dsn)) {
|
|
1709
1738
|
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
1710
1739
|
const expectedFormat = this.getSampleDSN();
|
|
@@ -1716,7 +1745,7 @@ Expected: ${expectedFormat}`
|
|
|
1716
1745
|
}
|
|
1717
1746
|
try {
|
|
1718
1747
|
const url = new SafeURL(dsn);
|
|
1719
|
-
const
|
|
1748
|
+
const connectionConfig = {
|
|
1720
1749
|
host: url.hostname,
|
|
1721
1750
|
port: url.port ? parseInt(url.port) : 3306,
|
|
1722
1751
|
database: url.pathname ? url.pathname.substring(1) : "",
|
|
@@ -1727,25 +1756,28 @@ Expected: ${expectedFormat}`
|
|
|
1727
1756
|
// Enable native multi-statement support
|
|
1728
1757
|
...connectionTimeoutSeconds !== void 0 && {
|
|
1729
1758
|
connectTimeout: connectionTimeoutSeconds * 1e3
|
|
1759
|
+
},
|
|
1760
|
+
...queryTimeoutSeconds !== void 0 && {
|
|
1761
|
+
queryTimeout: queryTimeoutSeconds * 1e3
|
|
1730
1762
|
}
|
|
1731
1763
|
};
|
|
1732
1764
|
url.forEachSearchParam((value, key) => {
|
|
1733
1765
|
if (key === "sslmode") {
|
|
1734
1766
|
if (value === "disable") {
|
|
1735
|
-
|
|
1767
|
+
connectionConfig.ssl = void 0;
|
|
1736
1768
|
} else if (value === "require") {
|
|
1737
|
-
|
|
1769
|
+
connectionConfig.ssl = { rejectUnauthorized: false };
|
|
1738
1770
|
} else {
|
|
1739
|
-
|
|
1771
|
+
connectionConfig.ssl = {};
|
|
1740
1772
|
}
|
|
1741
1773
|
}
|
|
1742
1774
|
});
|
|
1743
1775
|
if (url.password && url.password.includes("X-Amz-Credential")) {
|
|
1744
|
-
if (
|
|
1745
|
-
|
|
1776
|
+
if (connectionConfig.ssl === void 0) {
|
|
1777
|
+
connectionConfig.ssl = { rejectUnauthorized: false };
|
|
1746
1778
|
}
|
|
1747
1779
|
}
|
|
1748
|
-
return
|
|
1780
|
+
return connectionConfig;
|
|
1749
1781
|
} catch (error) {
|
|
1750
1782
|
throw new Error(
|
|
1751
1783
|
`Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -2244,7 +2276,7 @@ function getClientIdentifier(extra) {
|
|
|
2244
2276
|
|
|
2245
2277
|
// src/tools/execute-sql.ts
|
|
2246
2278
|
var executeSqlSchema = {
|
|
2247
|
-
sql: z.string().describe("SQL
|
|
2279
|
+
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
2248
2280
|
};
|
|
2249
2281
|
function splitSQLStatements(sql2) {
|
|
2250
2282
|
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
@@ -2306,12 +2338,12 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
2306
2338
|
// src/tools/search-objects.ts
|
|
2307
2339
|
import { z as z2 } from "zod";
|
|
2308
2340
|
var searchDatabaseObjectsSchema = {
|
|
2309
|
-
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("
|
|
2310
|
-
pattern: z2.string().optional().default("%").describe("
|
|
2311
|
-
schema: z2.string().optional().describe("Filter
|
|
2312
|
-
table: z2.string().optional().describe("Filter to
|
|
2313
|
-
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("
|
|
2314
|
-
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)")
|
|
2315
2347
|
};
|
|
2316
2348
|
function likePatternToRegex(pattern) {
|
|
2317
2349
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
@@ -2701,12 +2733,12 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
2701
2733
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2702
2734
|
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
|
|
2703
2735
|
const dbType = sourceConfig.type;
|
|
2704
|
-
const
|
|
2705
|
-
const
|
|
2706
|
-
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})`;
|
|
2707
2739
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
2708
2740
|
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
2709
|
-
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}`;
|
|
2710
2742
|
const isReadonly = executeOptions.readonly === true;
|
|
2711
2743
|
const annotations = {
|
|
2712
2744
|
title,
|
|
@@ -2726,10 +2758,14 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
2726
2758
|
annotations
|
|
2727
2759
|
};
|
|
2728
2760
|
}
|
|
2729
|
-
function getSearchObjectsMetadata(sourceId
|
|
2730
|
-
const
|
|
2731
|
-
const
|
|
2732
|
-
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`;
|
|
2733
2769
|
return {
|
|
2734
2770
|
name: toolName,
|
|
2735
2771
|
description,
|
|
@@ -2757,11 +2793,7 @@ function buildExecuteSqlTool(sourceId) {
|
|
|
2757
2793
|
};
|
|
2758
2794
|
}
|
|
2759
2795
|
function buildSearchObjectsTool(sourceId) {
|
|
2760
|
-
const
|
|
2761
|
-
const dbType = sourceConfig.type;
|
|
2762
|
-
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2763
|
-
const isDefault = sourceIds[0] === sourceId;
|
|
2764
|
-
const searchMetadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
|
|
2796
|
+
const searchMetadata = getSearchObjectsMetadata(sourceId);
|
|
2765
2797
|
return {
|
|
2766
2798
|
name: searchMetadata.name,
|
|
2767
2799
|
description: searchMetadata.description,
|
|
@@ -2770,31 +2802,37 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
2770
2802
|
name: "object_type",
|
|
2771
2803
|
type: "string",
|
|
2772
2804
|
required: true,
|
|
2773
|
-
description: "
|
|
2805
|
+
description: "Object type to search"
|
|
2774
2806
|
},
|
|
2775
2807
|
{
|
|
2776
2808
|
name: "pattern",
|
|
2777
2809
|
type: "string",
|
|
2778
2810
|
required: false,
|
|
2779
|
-
description: "
|
|
2811
|
+
description: "LIKE pattern (% = any chars, _ = one char). Default: %"
|
|
2780
2812
|
},
|
|
2781
2813
|
{
|
|
2782
2814
|
name: "schema",
|
|
2783
2815
|
type: "string",
|
|
2784
2816
|
required: false,
|
|
2785
|
-
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)"
|
|
2786
2824
|
},
|
|
2787
2825
|
{
|
|
2788
2826
|
name: "detail_level",
|
|
2789
2827
|
type: "string",
|
|
2790
2828
|
required: false,
|
|
2791
|
-
description: "
|
|
2829
|
+
description: "Detail: names (minimal), summary (metadata), full (all)"
|
|
2792
2830
|
},
|
|
2793
2831
|
{
|
|
2794
2832
|
name: "limit",
|
|
2795
2833
|
type: "integer",
|
|
2796
2834
|
required: false,
|
|
2797
|
-
description: "
|
|
2835
|
+
description: "Max results (default: 100, max: 1000)"
|
|
2798
2836
|
}
|
|
2799
2837
|
]
|
|
2800
2838
|
};
|
|
@@ -2940,21 +2978,18 @@ function registerTools(server) {
|
|
|
2940
2978
|
const registry = getToolRegistry();
|
|
2941
2979
|
for (const sourceId of sourceIds) {
|
|
2942
2980
|
const enabledTools = registry.getEnabledToolConfigs(sourceId);
|
|
2943
|
-
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2944
|
-
const dbType = sourceConfig.type;
|
|
2945
|
-
const isDefault = sourceIds[0] === sourceId;
|
|
2946
2981
|
for (const toolConfig of enabledTools) {
|
|
2947
2982
|
if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
|
|
2948
|
-
registerExecuteSqlTool(server, sourceId
|
|
2983
|
+
registerExecuteSqlTool(server, sourceId);
|
|
2949
2984
|
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
2950
|
-
registerSearchObjectsTool(server, sourceId
|
|
2985
|
+
registerSearchObjectsTool(server, sourceId);
|
|
2951
2986
|
} else {
|
|
2952
|
-
registerCustomTool(server,
|
|
2987
|
+
registerCustomTool(server, sourceId, toolConfig);
|
|
2953
2988
|
}
|
|
2954
2989
|
}
|
|
2955
2990
|
}
|
|
2956
2991
|
}
|
|
2957
|
-
function registerExecuteSqlTool(server, sourceId
|
|
2992
|
+
function registerExecuteSqlTool(server, sourceId) {
|
|
2958
2993
|
const metadata = getExecuteSqlMetadata(sourceId);
|
|
2959
2994
|
server.registerTool(
|
|
2960
2995
|
metadata.name,
|
|
@@ -2966,8 +3001,8 @@ function registerExecuteSqlTool(server, sourceId, dbType) {
|
|
|
2966
3001
|
createExecuteSqlToolHandler(sourceId)
|
|
2967
3002
|
);
|
|
2968
3003
|
}
|
|
2969
|
-
function registerSearchObjectsTool(server, sourceId
|
|
2970
|
-
const metadata = getSearchObjectsMetadata(sourceId
|
|
3004
|
+
function registerSearchObjectsTool(server, sourceId) {
|
|
3005
|
+
const metadata = getSearchObjectsMetadata(sourceId);
|
|
2971
3006
|
server.registerTool(
|
|
2972
3007
|
metadata.name,
|
|
2973
3008
|
{
|
|
@@ -2984,7 +3019,9 @@ function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
|
|
|
2984
3019
|
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
2985
3020
|
);
|
|
2986
3021
|
}
|
|
2987
|
-
function registerCustomTool(server,
|
|
3022
|
+
function registerCustomTool(server, sourceId, toolConfig) {
|
|
3023
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
3024
|
+
const dbType = sourceConfig.type;
|
|
2988
3025
|
const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
|
|
2989
3026
|
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
2990
3027
|
server.registerTool(
|
|
@@ -3006,7 +3043,7 @@ function registerCustomTool(server, toolConfig, dbType) {
|
|
|
3006
3043
|
}
|
|
3007
3044
|
|
|
3008
3045
|
// src/api/sources.ts
|
|
3009
|
-
function transformSourceConfig(source
|
|
3046
|
+
function transformSourceConfig(source) {
|
|
3010
3047
|
if (!source.type && source.dsn) {
|
|
3011
3048
|
const inferredType = getDatabaseTypeFromDSN(source.dsn);
|
|
3012
3049
|
if (inferredType) {
|
|
@@ -3018,8 +3055,7 @@ function transformSourceConfig(source, isDefault) {
|
|
|
3018
3055
|
}
|
|
3019
3056
|
const dataSource = {
|
|
3020
3057
|
id: source.id,
|
|
3021
|
-
type: source.type
|
|
3022
|
-
is_default: isDefault
|
|
3058
|
+
type: source.type
|
|
3023
3059
|
};
|
|
3024
3060
|
if (source.host) {
|
|
3025
3061
|
dataSource.host = source.host;
|
|
@@ -3058,9 +3094,8 @@ function transformSourceConfig(source, isDefault) {
|
|
|
3058
3094
|
function listSources(req, res) {
|
|
3059
3095
|
try {
|
|
3060
3096
|
const sourceConfigs = ConnectorManager.getAllSourceConfigs();
|
|
3061
|
-
const sources = sourceConfigs.map((config
|
|
3062
|
-
|
|
3063
|
-
return transformSourceConfig(config, isDefault);
|
|
3097
|
+
const sources = sourceConfigs.map((config) => {
|
|
3098
|
+
return transformSourceConfig(config);
|
|
3064
3099
|
});
|
|
3065
3100
|
res.json(sources);
|
|
3066
3101
|
} catch (error) {
|
|
@@ -3074,7 +3109,6 @@ function listSources(req, res) {
|
|
|
3074
3109
|
function getSource(req, res) {
|
|
3075
3110
|
try {
|
|
3076
3111
|
const sourceId = req.params.sourceId;
|
|
3077
|
-
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
3078
3112
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
3079
3113
|
if (!sourceConfig) {
|
|
3080
3114
|
const errorResponse = {
|
|
@@ -3084,8 +3118,7 @@ function getSource(req, res) {
|
|
|
3084
3118
|
res.status(404).json(errorResponse);
|
|
3085
3119
|
return;
|
|
3086
3120
|
}
|
|
3087
|
-
const
|
|
3088
|
-
const dataSource = transformSourceConfig(sourceConfig, isDefault);
|
|
3121
|
+
const dataSource = transformSourceConfig(sourceConfig);
|
|
3089
3122
|
res.json(dataSource);
|
|
3090
3123
|
} catch (error) {
|
|
3091
3124
|
console.error(`Error getting source ${req.params.sourceId}:`, error);
|
|
@@ -3280,13 +3313,8 @@ See documentation for more details on configuring database connections.
|
|
|
3280
3313
|
const connectorManager = new ConnectorManager();
|
|
3281
3314
|
const sources = sourceConfigsData.sources;
|
|
3282
3315
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
3283
|
-
console.error(`Connecting to ${sources.length} database source(s)...`);
|
|
3284
|
-
for (const source of sources) {
|
|
3285
|
-
const dsn = source.dsn || buildDSNFromSource(source);
|
|
3286
|
-
console.error(` - ${source.id}: ${redactDSN(dsn)}`);
|
|
3287
|
-
}
|
|
3288
3316
|
await connectorManager.connectWithSources(sources);
|
|
3289
|
-
const { initializeToolRegistry } = await import("./registry-
|
|
3317
|
+
const { initializeToolRegistry } = await import("./registry-FVGT25UH.js");
|
|
3290
3318
|
initializeToolRegistry({
|
|
3291
3319
|
sources: sourceConfigsData.sources,
|
|
3292
3320
|
tools: sourceConfigsData.tools
|
|
@@ -3380,9 +3408,9 @@ See documentation for more details on configuring database connections.
|
|
|
3380
3408
|
console.error(" Backend API: http://localhost:8080");
|
|
3381
3409
|
console.error("");
|
|
3382
3410
|
} else {
|
|
3383
|
-
console.error(`Admin console at http://
|
|
3411
|
+
console.error(`Admin console at http://localhost:${port}/`);
|
|
3384
3412
|
}
|
|
3385
|
-
console.error(`MCP server endpoint at http://
|
|
3413
|
+
console.error(`MCP server endpoint at http://localhost:${port}/mcp`);
|
|
3386
3414
|
});
|
|
3387
3415
|
} else {
|
|
3388
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",
|