@bytebase/dbhub 0.12.0 → 0.13.1
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 +10 -9
- package/dist/chunk-KBVJEDZF.js +1825 -0
- package/dist/index.js +738 -2286
- package/dist/public/assets/index-gVrYRID4.css +1 -0
- package/dist/public/assets/{index-CDt2HpUt.js → index-hd88eD9m.js} +11 -11
- package/dist/public/index.html +3 -3
- package/dist/registry-AWAIN6WO.js +10 -0
- package/package.json +20 -19
- package/dist/public/assets/index-C-kOl-8S.css +0 -1
package/dist/index.js
CHANGED
|
@@ -1,262 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
|
+
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
5
|
+
ConnectorManager,
|
|
6
|
+
ConnectorRegistry,
|
|
7
|
+
SafeURL,
|
|
8
|
+
buildDSNFromSource,
|
|
9
|
+
getDatabaseTypeFromDSN,
|
|
10
|
+
getDefaultPortForType,
|
|
11
|
+
getToolRegistry,
|
|
12
|
+
isDemoMode,
|
|
13
|
+
mapArgumentsToArray,
|
|
14
|
+
obfuscateDSNPassword,
|
|
15
|
+
parseConnectionInfoFromDSN,
|
|
16
|
+
redactDSN,
|
|
17
|
+
resolvePort,
|
|
18
|
+
resolveSourceConfigs,
|
|
19
|
+
resolveTransport,
|
|
20
|
+
stripCommentsAndStrings
|
|
21
|
+
} from "./chunk-KBVJEDZF.js";
|
|
2
22
|
|
|
3
23
|
// src/connectors/postgres/index.ts
|
|
4
24
|
import pg from "pg";
|
|
5
25
|
|
|
6
|
-
// src/connectors/interface.ts
|
|
7
|
-
var _ConnectorRegistry = class _ConnectorRegistry {
|
|
8
|
-
/**
|
|
9
|
-
* Register a new connector
|
|
10
|
-
*/
|
|
11
|
-
static register(connector) {
|
|
12
|
-
_ConnectorRegistry.connectors.set(connector.id, connector);
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Get a connector by ID
|
|
16
|
-
*/
|
|
17
|
-
static getConnector(id) {
|
|
18
|
-
return _ConnectorRegistry.connectors.get(id) || null;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Get connector for a DSN string
|
|
22
|
-
* Tries to find a connector that can handle the given DSN format
|
|
23
|
-
*/
|
|
24
|
-
static getConnectorForDSN(dsn) {
|
|
25
|
-
for (const connector of _ConnectorRegistry.connectors.values()) {
|
|
26
|
-
if (connector.dsnParser.isValidDSN(dsn)) {
|
|
27
|
-
return connector;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Get all available connector IDs
|
|
34
|
-
*/
|
|
35
|
-
static getAvailableConnectors() {
|
|
36
|
-
return Array.from(_ConnectorRegistry.connectors.keys());
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Get sample DSN for a specific connector
|
|
40
|
-
*/
|
|
41
|
-
static getSampleDSN(connectorType) {
|
|
42
|
-
const connector = _ConnectorRegistry.getConnector(connectorType);
|
|
43
|
-
if (!connector) return null;
|
|
44
|
-
return connector.dsnParser.getSampleDSN();
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Get all available sample DSNs
|
|
48
|
-
*/
|
|
49
|
-
static getAllSampleDSNs() {
|
|
50
|
-
const samples = {};
|
|
51
|
-
for (const [id, connector] of _ConnectorRegistry.connectors.entries()) {
|
|
52
|
-
samples[id] = connector.dsnParser.getSampleDSN();
|
|
53
|
-
}
|
|
54
|
-
return samples;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
_ConnectorRegistry.connectors = /* @__PURE__ */ new Map();
|
|
58
|
-
var ConnectorRegistry = _ConnectorRegistry;
|
|
59
|
-
|
|
60
|
-
// src/utils/safe-url.ts
|
|
61
|
-
var SafeURL = class {
|
|
62
|
-
/**
|
|
63
|
-
* Parse a URL and handle special characters in passwords
|
|
64
|
-
* This is a safe alternative to the URL constructor
|
|
65
|
-
*
|
|
66
|
-
* @param urlString - The DSN string to parse
|
|
67
|
-
*/
|
|
68
|
-
constructor(urlString) {
|
|
69
|
-
this.protocol = "";
|
|
70
|
-
this.hostname = "";
|
|
71
|
-
this.port = "";
|
|
72
|
-
this.pathname = "";
|
|
73
|
-
this.username = "";
|
|
74
|
-
this.password = "";
|
|
75
|
-
this.searchParams = /* @__PURE__ */ new Map();
|
|
76
|
-
if (!urlString || urlString.trim() === "") {
|
|
77
|
-
throw new Error("URL string cannot be empty");
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
const protocolSeparator = urlString.indexOf("://");
|
|
81
|
-
if (protocolSeparator !== -1) {
|
|
82
|
-
this.protocol = urlString.substring(0, protocolSeparator + 1);
|
|
83
|
-
urlString = urlString.substring(protocolSeparator + 3);
|
|
84
|
-
} else {
|
|
85
|
-
throw new Error('Invalid URL format: missing protocol (e.g., "mysql://")');
|
|
86
|
-
}
|
|
87
|
-
const questionMarkIndex = urlString.indexOf("?");
|
|
88
|
-
let queryParams = "";
|
|
89
|
-
if (questionMarkIndex !== -1) {
|
|
90
|
-
queryParams = urlString.substring(questionMarkIndex + 1);
|
|
91
|
-
urlString = urlString.substring(0, questionMarkIndex);
|
|
92
|
-
queryParams.split("&").forEach((pair) => {
|
|
93
|
-
const parts = pair.split("=");
|
|
94
|
-
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
95
|
-
this.searchParams.set(parts[0], decodeURIComponent(parts[1]));
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
const atIndex = urlString.indexOf("@");
|
|
100
|
-
if (atIndex !== -1) {
|
|
101
|
-
const auth = urlString.substring(0, atIndex);
|
|
102
|
-
urlString = urlString.substring(atIndex + 1);
|
|
103
|
-
const colonIndex2 = auth.indexOf(":");
|
|
104
|
-
if (colonIndex2 !== -1) {
|
|
105
|
-
this.username = auth.substring(0, colonIndex2);
|
|
106
|
-
this.password = auth.substring(colonIndex2 + 1);
|
|
107
|
-
this.username = decodeURIComponent(this.username);
|
|
108
|
-
this.password = decodeURIComponent(this.password);
|
|
109
|
-
} else {
|
|
110
|
-
this.username = auth;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const pathSeparatorIndex = urlString.indexOf("/");
|
|
114
|
-
if (pathSeparatorIndex !== -1) {
|
|
115
|
-
this.pathname = urlString.substring(pathSeparatorIndex);
|
|
116
|
-
urlString = urlString.substring(0, pathSeparatorIndex);
|
|
117
|
-
}
|
|
118
|
-
const colonIndex = urlString.indexOf(":");
|
|
119
|
-
if (colonIndex !== -1) {
|
|
120
|
-
this.hostname = urlString.substring(0, colonIndex);
|
|
121
|
-
this.port = urlString.substring(colonIndex + 1);
|
|
122
|
-
} else {
|
|
123
|
-
this.hostname = urlString;
|
|
124
|
-
}
|
|
125
|
-
if (this.protocol === "") {
|
|
126
|
-
throw new Error("Invalid URL: protocol is required");
|
|
127
|
-
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
throw new Error(`Failed to parse URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Helper method to safely get a parameter from query string
|
|
134
|
-
*
|
|
135
|
-
* @param name - The parameter name to retrieve
|
|
136
|
-
* @returns The parameter value or null if not found
|
|
137
|
-
*/
|
|
138
|
-
getSearchParam(name) {
|
|
139
|
-
return this.searchParams.has(name) ? this.searchParams.get(name) : null;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Helper method to iterate over all parameters
|
|
143
|
-
*
|
|
144
|
-
* @param callback - Function to call for each parameter
|
|
145
|
-
*/
|
|
146
|
-
forEachSearchParam(callback) {
|
|
147
|
-
this.searchParams.forEach((value, key) => callback(value, key));
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// src/utils/dsn-obfuscate.ts
|
|
152
|
-
function parseConnectionInfoFromDSN(dsn) {
|
|
153
|
-
if (!dsn) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const type = getDatabaseTypeFromDSN(dsn);
|
|
158
|
-
if (typeof type === "undefined") {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
if (type === "sqlite") {
|
|
162
|
-
const prefix = "sqlite:///";
|
|
163
|
-
if (dsn.length > prefix.length) {
|
|
164
|
-
const rawPath = dsn.substring(prefix.length);
|
|
165
|
-
const firstChar = rawPath[0];
|
|
166
|
-
const isWindowsDrive = rawPath.length > 1 && rawPath[1] === ":";
|
|
167
|
-
const isSpecialPath = firstChar === ":" || firstChar === "." || firstChar === "~" || isWindowsDrive;
|
|
168
|
-
return {
|
|
169
|
-
type,
|
|
170
|
-
database: isSpecialPath ? rawPath : "/" + rawPath
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
return { type };
|
|
174
|
-
}
|
|
175
|
-
const url = new SafeURL(dsn);
|
|
176
|
-
const info = { type };
|
|
177
|
-
if (url.hostname) {
|
|
178
|
-
info.host = url.hostname;
|
|
179
|
-
}
|
|
180
|
-
if (url.port) {
|
|
181
|
-
info.port = parseInt(url.port, 10);
|
|
182
|
-
}
|
|
183
|
-
if (url.pathname && url.pathname.length > 1) {
|
|
184
|
-
info.database = url.pathname.substring(1);
|
|
185
|
-
}
|
|
186
|
-
if (url.username) {
|
|
187
|
-
info.user = url.username;
|
|
188
|
-
}
|
|
189
|
-
return info;
|
|
190
|
-
} catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function obfuscateDSNPassword(dsn) {
|
|
195
|
-
if (!dsn) {
|
|
196
|
-
return dsn;
|
|
197
|
-
}
|
|
198
|
-
try {
|
|
199
|
-
const type = getDatabaseTypeFromDSN(dsn);
|
|
200
|
-
if (type === "sqlite") {
|
|
201
|
-
return dsn;
|
|
202
|
-
}
|
|
203
|
-
const url = new SafeURL(dsn);
|
|
204
|
-
if (!url.password) {
|
|
205
|
-
return dsn;
|
|
206
|
-
}
|
|
207
|
-
const obfuscatedPassword = "*".repeat(Math.min(url.password.length, 8));
|
|
208
|
-
const protocol = dsn.split(":")[0];
|
|
209
|
-
let result;
|
|
210
|
-
if (url.username) {
|
|
211
|
-
result = `${protocol}://${url.username}:${obfuscatedPassword}@${url.hostname}`;
|
|
212
|
-
} else {
|
|
213
|
-
result = `${protocol}://${obfuscatedPassword}@${url.hostname}`;
|
|
214
|
-
}
|
|
215
|
-
if (url.port) {
|
|
216
|
-
result += `:${url.port}`;
|
|
217
|
-
}
|
|
218
|
-
result += url.pathname;
|
|
219
|
-
if (url.searchParams.size > 0) {
|
|
220
|
-
const params = [];
|
|
221
|
-
url.forEachSearchParam((value, key) => {
|
|
222
|
-
params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
223
|
-
});
|
|
224
|
-
result += `?${params.join("&")}`;
|
|
225
|
-
}
|
|
226
|
-
return result;
|
|
227
|
-
} catch {
|
|
228
|
-
return dsn;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
function getDatabaseTypeFromDSN(dsn) {
|
|
232
|
-
if (!dsn) {
|
|
233
|
-
return void 0;
|
|
234
|
-
}
|
|
235
|
-
const protocol = dsn.split(":")[0];
|
|
236
|
-
return protocolToConnectorType(protocol);
|
|
237
|
-
}
|
|
238
|
-
function protocolToConnectorType(protocol) {
|
|
239
|
-
const mapping = {
|
|
240
|
-
"postgres": "postgres",
|
|
241
|
-
"postgresql": "postgres",
|
|
242
|
-
"mysql": "mysql",
|
|
243
|
-
"mariadb": "mariadb",
|
|
244
|
-
"sqlserver": "sqlserver",
|
|
245
|
-
"sqlite": "sqlite"
|
|
246
|
-
};
|
|
247
|
-
return mapping[protocol];
|
|
248
|
-
}
|
|
249
|
-
function getDefaultPortForType(type) {
|
|
250
|
-
const ports = {
|
|
251
|
-
"postgres": 5432,
|
|
252
|
-
"mysql": 3306,
|
|
253
|
-
"mariadb": 3306,
|
|
254
|
-
"sqlserver": 1433,
|
|
255
|
-
"sqlite": void 0
|
|
256
|
-
};
|
|
257
|
-
return ports[type];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
26
|
// src/utils/sql-row-limiter.ts
|
|
261
27
|
var SQLRowLimiter = class {
|
|
262
28
|
/**
|
|
@@ -268,34 +34,42 @@ var SQLRowLimiter = class {
|
|
|
268
34
|
return trimmed.startsWith("select");
|
|
269
35
|
}
|
|
270
36
|
/**
|
|
271
|
-
* Check if a SQL statement already has a LIMIT clause
|
|
37
|
+
* Check if a SQL statement already has a LIMIT clause.
|
|
38
|
+
* Strips comments and string literals first to avoid false positives.
|
|
272
39
|
*/
|
|
273
40
|
static hasLimitClause(sql2) {
|
|
274
|
-
const
|
|
275
|
-
|
|
41
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
42
|
+
const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
|
|
43
|
+
return limitRegex.test(cleanedSQL);
|
|
276
44
|
}
|
|
277
45
|
/**
|
|
278
|
-
* Check if a SQL statement already has a TOP clause (SQL Server)
|
|
46
|
+
* Check if a SQL statement already has a TOP clause (SQL Server).
|
|
47
|
+
* Strips comments and string literals first to avoid false positives.
|
|
279
48
|
*/
|
|
280
49
|
static hasTopClause(sql2) {
|
|
50
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
281
51
|
const topRegex = /\bselect\s+top\s+\d+/i;
|
|
282
|
-
return topRegex.test(
|
|
52
|
+
return topRegex.test(cleanedSQL);
|
|
283
53
|
}
|
|
284
54
|
/**
|
|
285
|
-
* Extract existing LIMIT value from SQL if present
|
|
55
|
+
* Extract existing LIMIT value from SQL if present.
|
|
56
|
+
* Strips comments and string literals first to avoid false positives.
|
|
286
57
|
*/
|
|
287
58
|
static extractLimitValue(sql2) {
|
|
288
|
-
const
|
|
59
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
60
|
+
const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
|
|
289
61
|
if (limitMatch) {
|
|
290
62
|
return parseInt(limitMatch[1], 10);
|
|
291
63
|
}
|
|
292
64
|
return null;
|
|
293
65
|
}
|
|
294
66
|
/**
|
|
295
|
-
* Extract existing TOP value from SQL if present (SQL Server)
|
|
67
|
+
* Extract existing TOP value from SQL if present (SQL Server).
|
|
68
|
+
* Strips comments and string literals first to avoid false positives.
|
|
296
69
|
*/
|
|
297
70
|
static extractTopValue(sql2) {
|
|
298
|
-
const
|
|
71
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
72
|
+
const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
|
|
299
73
|
if (topMatch) {
|
|
300
74
|
return parseInt(topMatch[1], 10);
|
|
301
75
|
}
|
|
@@ -328,13 +102,34 @@ var SQLRowLimiter = class {
|
|
|
328
102
|
return sql2.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
329
103
|
}
|
|
330
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if a LIMIT clause uses a parameter placeholder (not a literal number).
|
|
107
|
+
* Strips comments and string literals first to avoid false positives.
|
|
108
|
+
*/
|
|
109
|
+
static hasParameterizedLimit(sql2) {
|
|
110
|
+
const cleanedSQL = stripCommentsAndStrings(sql2);
|
|
111
|
+
const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
|
|
112
|
+
return parameterizedLimitRegex.test(cleanedSQL);
|
|
113
|
+
}
|
|
331
114
|
/**
|
|
332
115
|
* Apply maxRows limit to a SELECT query only
|
|
116
|
+
*
|
|
117
|
+
* This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
|
|
118
|
+
* the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
|
|
119
|
+
*
|
|
120
|
+
* For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
|
|
121
|
+
* to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
|
|
333
122
|
*/
|
|
334
123
|
static applyMaxRows(sql2, maxRows) {
|
|
335
124
|
if (!maxRows || !this.isSelectQuery(sql2)) {
|
|
336
125
|
return sql2;
|
|
337
126
|
}
|
|
127
|
+
if (this.hasParameterizedLimit(sql2)) {
|
|
128
|
+
const trimmed = sql2.trim();
|
|
129
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
130
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
131
|
+
return `SELECT * FROM (${sqlWithoutSemicolon}) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
132
|
+
}
|
|
338
133
|
return this.applyLimitToQuery(sql2, maxRows);
|
|
339
134
|
}
|
|
340
135
|
/**
|
|
@@ -410,6 +205,11 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
410
205
|
this.name = "PostgreSQL";
|
|
411
206
|
this.dsnParser = new PostgresDSNParser();
|
|
412
207
|
this.pool = null;
|
|
208
|
+
// Source ID is set by ConnectorManager after cloning
|
|
209
|
+
this.sourceId = "default";
|
|
210
|
+
}
|
|
211
|
+
getId() {
|
|
212
|
+
return this.sourceId;
|
|
413
213
|
}
|
|
414
214
|
clone() {
|
|
415
215
|
return new _PostgresConnector();
|
|
@@ -422,7 +222,6 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
422
222
|
}
|
|
423
223
|
this.pool = new Pool(poolConfig);
|
|
424
224
|
const client = await this.pool.connect();
|
|
425
|
-
console.error("Successfully connected to PostgreSQL database");
|
|
426
225
|
client.release();
|
|
427
226
|
} catch (err) {
|
|
428
227
|
console.error("Failed to connect to PostgreSQL database:", err);
|
|
@@ -667,7 +466,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
667
466
|
client.release();
|
|
668
467
|
}
|
|
669
468
|
}
|
|
670
|
-
async executeSQL(sql2, options) {
|
|
469
|
+
async executeSQL(sql2, options, parameters) {
|
|
671
470
|
if (!this.pool) {
|
|
672
471
|
throw new Error("Not connected to database");
|
|
673
472
|
}
|
|
@@ -676,8 +475,21 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
676
475
|
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
677
476
|
if (statements.length === 1) {
|
|
678
477
|
const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
|
|
478
|
+
if (parameters && parameters.length > 0) {
|
|
479
|
+
try {
|
|
480
|
+
return await client.query(processedStatement, parameters);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
|
|
483
|
+
console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
|
|
484
|
+
console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
679
488
|
return await client.query(processedStatement);
|
|
680
489
|
} else {
|
|
490
|
+
if (parameters && parameters.length > 0) {
|
|
491
|
+
throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
|
|
492
|
+
}
|
|
681
493
|
let allRows = [];
|
|
682
494
|
await client.query("BEGIN");
|
|
683
495
|
try {
|
|
@@ -800,6 +612,11 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
800
612
|
this.id = "sqlserver";
|
|
801
613
|
this.name = "SQL Server";
|
|
802
614
|
this.dsnParser = new SQLServerDSNParser();
|
|
615
|
+
// Source ID is set by ConnectorManager after cloning
|
|
616
|
+
this.sourceId = "default";
|
|
617
|
+
}
|
|
618
|
+
getId() {
|
|
619
|
+
return this.sourceId;
|
|
803
620
|
}
|
|
804
621
|
clone() {
|
|
805
622
|
return new _SQLServerConnector();
|
|
@@ -1042,16 +859,49 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
1042
859
|
throw new Error(`Failed to get stored procedure details: ${error.message}`);
|
|
1043
860
|
}
|
|
1044
861
|
}
|
|
1045
|
-
async executeSQL(
|
|
862
|
+
async executeSQL(sqlQuery, options, parameters) {
|
|
1046
863
|
if (!this.connection) {
|
|
1047
864
|
throw new Error("Not connected to SQL Server database");
|
|
1048
865
|
}
|
|
1049
866
|
try {
|
|
1050
|
-
let processedSQL =
|
|
867
|
+
let processedSQL = sqlQuery;
|
|
1051
868
|
if (options.maxRows) {
|
|
1052
|
-
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(
|
|
869
|
+
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
|
|
870
|
+
}
|
|
871
|
+
const request = this.connection.request();
|
|
872
|
+
if (parameters && parameters.length > 0) {
|
|
873
|
+
parameters.forEach((param, index) => {
|
|
874
|
+
const paramName = `p${index + 1}`;
|
|
875
|
+
if (typeof param === "string") {
|
|
876
|
+
request.input(paramName, sql.VarChar, param);
|
|
877
|
+
} else if (typeof param === "number") {
|
|
878
|
+
if (Number.isInteger(param)) {
|
|
879
|
+
request.input(paramName, sql.Int, param);
|
|
880
|
+
} else {
|
|
881
|
+
request.input(paramName, sql.Float, param);
|
|
882
|
+
}
|
|
883
|
+
} else if (typeof param === "boolean") {
|
|
884
|
+
request.input(paramName, sql.Bit, param);
|
|
885
|
+
} else if (param === null || param === void 0) {
|
|
886
|
+
request.input(paramName, sql.VarChar, param);
|
|
887
|
+
} else if (Array.isArray(param)) {
|
|
888
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
889
|
+
} else {
|
|
890
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
let result;
|
|
895
|
+
try {
|
|
896
|
+
result = await request.query(processedSQL);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (parameters && parameters.length > 0) {
|
|
899
|
+
console.error(`[SQL Server executeSQL] ERROR: ${error.message}`);
|
|
900
|
+
console.error(`[SQL Server executeSQL] SQL: ${processedSQL}`);
|
|
901
|
+
console.error(`[SQL Server executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
902
|
+
}
|
|
903
|
+
throw error;
|
|
1053
904
|
}
|
|
1054
|
-
const result = await this.connection.request().query(processedSQL);
|
|
1055
905
|
return {
|
|
1056
906
|
rows: result.recordset || [],
|
|
1057
907
|
fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
|
|
@@ -1151,8 +1001,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1151
1001
|
this.dsnParser = new SQLiteDSNParser();
|
|
1152
1002
|
this.db = null;
|
|
1153
1003
|
this.dbPath = ":memory:";
|
|
1004
|
+
// Default to in-memory database
|
|
1005
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1006
|
+
this.sourceId = "default";
|
|
1007
|
+
}
|
|
1008
|
+
getId() {
|
|
1009
|
+
return this.sourceId;
|
|
1154
1010
|
}
|
|
1155
|
-
// Default to in-memory database
|
|
1156
1011
|
clone() {
|
|
1157
1012
|
return new _SQLiteConnector();
|
|
1158
1013
|
}
|
|
@@ -1170,10 +1025,8 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1170
1025
|
dbOptions.readonly = true;
|
|
1171
1026
|
}
|
|
1172
1027
|
this.db = new Database(this.dbPath, dbOptions);
|
|
1173
|
-
console.error("Successfully connected to SQLite database");
|
|
1174
1028
|
if (initScript) {
|
|
1175
1029
|
this.db.exec(initScript);
|
|
1176
|
-
console.error("Successfully initialized database with script");
|
|
1177
1030
|
}
|
|
1178
1031
|
} catch (error) {
|
|
1179
1032
|
console.error("Failed to connect to SQLite database:", error);
|
|
@@ -1320,7 +1173,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1320
1173
|
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
1321
1174
|
);
|
|
1322
1175
|
}
|
|
1323
|
-
async executeSQL(sql2, options) {
|
|
1176
|
+
async executeSQL(sql2, options, parameters) {
|
|
1324
1177
|
if (!this.db) {
|
|
1325
1178
|
throw new Error("Not connected to SQLite database");
|
|
1326
1179
|
}
|
|
@@ -1334,13 +1187,39 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1334
1187
|
processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows);
|
|
1335
1188
|
}
|
|
1336
1189
|
if (isReadStatement) {
|
|
1337
|
-
|
|
1338
|
-
|
|
1190
|
+
if (parameters && parameters.length > 0) {
|
|
1191
|
+
try {
|
|
1192
|
+
const rows = this.db.prepare(processedStatement).all(...parameters);
|
|
1193
|
+
return { rows };
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
1196
|
+
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
1197
|
+
console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
} else {
|
|
1201
|
+
const rows = this.db.prepare(processedStatement).all();
|
|
1202
|
+
return { rows };
|
|
1203
|
+
}
|
|
1339
1204
|
} else {
|
|
1340
|
-
|
|
1205
|
+
if (parameters && parameters.length > 0) {
|
|
1206
|
+
try {
|
|
1207
|
+
this.db.prepare(processedStatement).run(...parameters);
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
1210
|
+
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
1211
|
+
console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1212
|
+
throw error;
|
|
1213
|
+
}
|
|
1214
|
+
} else {
|
|
1215
|
+
this.db.prepare(processedStatement).run();
|
|
1216
|
+
}
|
|
1341
1217
|
return { rows: [] };
|
|
1342
1218
|
}
|
|
1343
1219
|
} else {
|
|
1220
|
+
if (parameters && parameters.length > 0) {
|
|
1221
|
+
throw new Error("Parameters are not supported for multi-statement queries in SQLite");
|
|
1222
|
+
}
|
|
1344
1223
|
const readStatements = [];
|
|
1345
1224
|
const writeStatements = [];
|
|
1346
1225
|
for (const statement of statements) {
|
|
@@ -1472,6 +1351,11 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1472
1351
|
this.name = "MySQL";
|
|
1473
1352
|
this.dsnParser = new MySQLDSNParser();
|
|
1474
1353
|
this.pool = null;
|
|
1354
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1355
|
+
this.sourceId = "default";
|
|
1356
|
+
}
|
|
1357
|
+
getId() {
|
|
1358
|
+
return this.sourceId;
|
|
1475
1359
|
}
|
|
1476
1360
|
clone() {
|
|
1477
1361
|
return new _MySQLConnector();
|
|
@@ -1481,7 +1365,6 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1481
1365
|
const connectionOptions = await this.dsnParser.parse(dsn, config);
|
|
1482
1366
|
this.pool = mysql.createPool(connectionOptions);
|
|
1483
1367
|
const [rows] = await this.pool.query("SELECT 1");
|
|
1484
|
-
console.error("Successfully connected to MySQL database");
|
|
1485
1368
|
} catch (err) {
|
|
1486
1369
|
console.error("Failed to connect to MySQL database:", err);
|
|
1487
1370
|
throw err;
|
|
@@ -1763,7 +1646,7 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1763
1646
|
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
1764
1647
|
return rows[0].DB;
|
|
1765
1648
|
}
|
|
1766
|
-
async executeSQL(sql2, options) {
|
|
1649
|
+
async executeSQL(sql2, options, parameters) {
|
|
1767
1650
|
if (!this.pool) {
|
|
1768
1651
|
throw new Error("Not connected to database");
|
|
1769
1652
|
}
|
|
@@ -1780,7 +1663,19 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1780
1663
|
processedSQL += ";";
|
|
1781
1664
|
}
|
|
1782
1665
|
}
|
|
1783
|
-
|
|
1666
|
+
let results;
|
|
1667
|
+
if (parameters && parameters.length > 0) {
|
|
1668
|
+
try {
|
|
1669
|
+
results = await conn.query(processedSQL, parameters);
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
console.error(`[MySQL executeSQL] ERROR: ${error.message}`);
|
|
1672
|
+
console.error(`[MySQL executeSQL] SQL: ${processedSQL}`);
|
|
1673
|
+
console.error(`[MySQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
1674
|
+
throw error;
|
|
1675
|
+
}
|
|
1676
|
+
} else {
|
|
1677
|
+
results = await conn.query(processedSQL);
|
|
1678
|
+
}
|
|
1784
1679
|
const [firstResult] = results;
|
|
1785
1680
|
const rows = parseQueryResults(firstResult);
|
|
1786
1681
|
return { rows };
|
|
@@ -1859,6 +1754,11 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
1859
1754
|
this.name = "MariaDB";
|
|
1860
1755
|
this.dsnParser = new MariadbDSNParser();
|
|
1861
1756
|
this.pool = null;
|
|
1757
|
+
// Source ID is set by ConnectorManager after cloning
|
|
1758
|
+
this.sourceId = "default";
|
|
1759
|
+
}
|
|
1760
|
+
getId() {
|
|
1761
|
+
return this.sourceId;
|
|
1862
1762
|
}
|
|
1863
1763
|
clone() {
|
|
1864
1764
|
return new _MariaDBConnector();
|
|
@@ -1867,9 +1767,7 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
1867
1767
|
try {
|
|
1868
1768
|
const connectionConfig = await this.dsnParser.parse(dsn, config);
|
|
1869
1769
|
this.pool = mariadb.createPool(connectionConfig);
|
|
1870
|
-
console.error("Testing connection to MariaDB...");
|
|
1871
1770
|
await this.pool.query("SELECT 1");
|
|
1872
|
-
console.error("Successfully connected to MariaDB database");
|
|
1873
1771
|
} catch (err) {
|
|
1874
1772
|
console.error("Failed to connect to MariaDB database:", err);
|
|
1875
1773
|
throw err;
|
|
@@ -2151,7 +2049,7 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
2151
2049
|
const rows = await this.pool.query("SELECT DATABASE() AS DB");
|
|
2152
2050
|
return rows[0].DB;
|
|
2153
2051
|
}
|
|
2154
|
-
async executeSQL(sql2, options) {
|
|
2052
|
+
async executeSQL(sql2, options, parameters) {
|
|
2155
2053
|
if (!this.pool) {
|
|
2156
2054
|
throw new Error("Not connected to database");
|
|
2157
2055
|
}
|
|
@@ -2168,7 +2066,19 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
2168
2066
|
processedSQL += ";";
|
|
2169
2067
|
}
|
|
2170
2068
|
}
|
|
2171
|
-
|
|
2069
|
+
let results;
|
|
2070
|
+
if (parameters && parameters.length > 0) {
|
|
2071
|
+
try {
|
|
2072
|
+
results = await conn.query(processedSQL, parameters);
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
console.error(`[MariaDB executeSQL] ERROR: ${error.message}`);
|
|
2075
|
+
console.error(`[MariaDB executeSQL] SQL: ${processedSQL}`);
|
|
2076
|
+
console.error(`[MariaDB executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
2077
|
+
throw error;
|
|
2078
|
+
}
|
|
2079
|
+
} else {
|
|
2080
|
+
results = await conn.query(processedSQL);
|
|
2081
|
+
}
|
|
2172
2082
|
const rows = parseQueryResults(results);
|
|
2173
2083
|
return { rows };
|
|
2174
2084
|
} catch (error) {
|
|
@@ -2183,1530 +2093,132 @@ var mariadbConnector = new MariaDBConnector();
|
|
|
2183
2093
|
ConnectorRegistry.register(mariadbConnector);
|
|
2184
2094
|
|
|
2185
2095
|
// src/server.ts
|
|
2186
|
-
import { McpServer
|
|
2096
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2187
2097
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2188
2098
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2189
2099
|
import express from "express";
|
|
2190
|
-
import
|
|
2191
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
2192
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2193
|
-
|
|
2194
|
-
// src/utils/ssh-tunnel.ts
|
|
2195
|
-
import { Client } from "ssh2";
|
|
2100
|
+
import path from "path";
|
|
2196
2101
|
import { readFileSync } from "fs";
|
|
2197
|
-
import {
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2102
|
+
import { fileURLToPath } from "url";
|
|
2103
|
+
|
|
2104
|
+
// src/tools/execute-sql.ts
|
|
2105
|
+
import { z } from "zod";
|
|
2106
|
+
|
|
2107
|
+
// src/utils/response-formatter.ts
|
|
2108
|
+
function bigIntReplacer(_key, value) {
|
|
2109
|
+
if (typeof value === "bigint") {
|
|
2110
|
+
return value.toString();
|
|
2204
2111
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
sshConfig.passphrase = config.passphrase;
|
|
2230
|
-
}
|
|
2231
|
-
} catch (error) {
|
|
2232
|
-
reject(new Error(`Failed to read private key file: ${error instanceof Error ? error.message : String(error)}`));
|
|
2233
|
-
return;
|
|
2234
|
-
}
|
|
2235
|
-
} else {
|
|
2236
|
-
reject(new Error("Either password or privateKey must be provided for SSH authentication"));
|
|
2237
|
-
return;
|
|
2112
|
+
return value;
|
|
2113
|
+
}
|
|
2114
|
+
function formatSuccessResponse(data, meta = {}) {
|
|
2115
|
+
return {
|
|
2116
|
+
success: true,
|
|
2117
|
+
data,
|
|
2118
|
+
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
function formatErrorResponse(error, code = "ERROR", details) {
|
|
2122
|
+
return {
|
|
2123
|
+
success: false,
|
|
2124
|
+
error,
|
|
2125
|
+
code,
|
|
2126
|
+
...details ? { details } : {}
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
function createToolErrorResponse(error, code = "ERROR", details) {
|
|
2130
|
+
return {
|
|
2131
|
+
content: [
|
|
2132
|
+
{
|
|
2133
|
+
type: "text",
|
|
2134
|
+
text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
|
|
2135
|
+
mimeType: "application/json"
|
|
2238
2136
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
localPort: address.port,
|
|
2279
|
-
targetHost: options.targetHost,
|
|
2280
|
-
targetPort: options.targetPort
|
|
2281
|
-
};
|
|
2282
|
-
this.isConnected = true;
|
|
2283
|
-
console.error(`SSH tunnel established: localhost:${address.port} -> ${options.targetHost}:${options.targetPort}`);
|
|
2284
|
-
resolve(this.tunnelInfo);
|
|
2285
|
-
});
|
|
2286
|
-
this.localServer.on("error", (err) => {
|
|
2287
|
-
this.cleanup();
|
|
2288
|
-
reject(new Error(`Local server error: ${err.message}`));
|
|
2289
|
-
});
|
|
2290
|
-
});
|
|
2291
|
-
this.sshClient.connect(sshConfig);
|
|
2292
|
-
});
|
|
2137
|
+
],
|
|
2138
|
+
isError: true
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
function createToolSuccessResponse(data, meta = {}) {
|
|
2142
|
+
return {
|
|
2143
|
+
content: [
|
|
2144
|
+
{
|
|
2145
|
+
type: "text",
|
|
2146
|
+
text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
|
|
2147
|
+
mimeType: "application/json"
|
|
2148
|
+
}
|
|
2149
|
+
]
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/utils/allowed-keywords.ts
|
|
2154
|
+
var allowedKeywords = {
|
|
2155
|
+
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
2156
|
+
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2157
|
+
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2158
|
+
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
2159
|
+
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2160
|
+
};
|
|
2161
|
+
function isReadOnlySQL(sql2, connectorType) {
|
|
2162
|
+
const cleanedSQL = stripCommentsAndStrings(sql2).trim().toLowerCase();
|
|
2163
|
+
if (!cleanedSQL) {
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
const firstWord = cleanedSQL.split(/\s+/)[0];
|
|
2167
|
+
const keywordList = allowedKeywords[connectorType] || [];
|
|
2168
|
+
return keywordList.includes(firstWord);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// src/requests/store.ts
|
|
2172
|
+
var RequestStore = class {
|
|
2173
|
+
constructor() {
|
|
2174
|
+
this.store = /* @__PURE__ */ new Map();
|
|
2175
|
+
this.maxPerSource = 100;
|
|
2293
2176
|
}
|
|
2294
2177
|
/**
|
|
2295
|
-
*
|
|
2178
|
+
* Add a request to the store
|
|
2179
|
+
* Evicts oldest entry if at capacity
|
|
2296
2180
|
*/
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2181
|
+
add(request) {
|
|
2182
|
+
const requests = this.store.get(request.sourceId) ?? [];
|
|
2183
|
+
requests.push(request);
|
|
2184
|
+
if (requests.length > this.maxPerSource) {
|
|
2185
|
+
requests.shift();
|
|
2300
2186
|
}
|
|
2301
|
-
|
|
2302
|
-
this.cleanup();
|
|
2303
|
-
this.isConnected = false;
|
|
2304
|
-
console.error("SSH tunnel closed");
|
|
2305
|
-
resolve();
|
|
2306
|
-
});
|
|
2187
|
+
this.store.set(request.sourceId, requests);
|
|
2307
2188
|
}
|
|
2308
2189
|
/**
|
|
2309
|
-
*
|
|
2190
|
+
* Get requests, optionally filtered by source
|
|
2191
|
+
* Returns newest first
|
|
2310
2192
|
*/
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
this.
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
this.sshClient.end();
|
|
2318
|
-
this.sshClient = null;
|
|
2193
|
+
getAll(sourceId) {
|
|
2194
|
+
let requests;
|
|
2195
|
+
if (sourceId) {
|
|
2196
|
+
requests = [...this.store.get(sourceId) ?? []];
|
|
2197
|
+
} else {
|
|
2198
|
+
requests = Array.from(this.store.values()).flat();
|
|
2319
2199
|
}
|
|
2320
|
-
|
|
2200
|
+
return requests.sort(
|
|
2201
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
2202
|
+
);
|
|
2321
2203
|
}
|
|
2322
2204
|
/**
|
|
2323
|
-
* Get
|
|
2205
|
+
* Get total count of requests across all sources
|
|
2324
2206
|
*/
|
|
2325
|
-
|
|
2326
|
-
return this.
|
|
2207
|
+
getTotal() {
|
|
2208
|
+
return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
|
|
2327
2209
|
}
|
|
2328
2210
|
/**
|
|
2329
|
-
*
|
|
2211
|
+
* Clear all requests (useful for testing)
|
|
2330
2212
|
*/
|
|
2331
|
-
|
|
2332
|
-
|
|
2213
|
+
clear() {
|
|
2214
|
+
this.store.clear();
|
|
2333
2215
|
}
|
|
2334
2216
|
};
|
|
2335
2217
|
|
|
2336
|
-
// src/
|
|
2337
|
-
|
|
2338
|
-
import path2 from "path";
|
|
2339
|
-
import { homedir as homedir3 } from "os";
|
|
2340
|
-
import toml from "@iarna/toml";
|
|
2218
|
+
// src/requests/index.ts
|
|
2219
|
+
var requestStore = new RequestStore();
|
|
2341
2220
|
|
|
2342
|
-
// src/
|
|
2343
|
-
import dotenv from "dotenv";
|
|
2344
|
-
import path from "path";
|
|
2345
|
-
import fs from "fs";
|
|
2346
|
-
import { fileURLToPath } from "url";
|
|
2347
|
-
import { homedir as homedir2 } from "os";
|
|
2348
|
-
|
|
2349
|
-
// src/utils/ssh-config-parser.ts
|
|
2350
|
-
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
2351
|
-
import { homedir } from "os";
|
|
2352
|
-
import { join } from "path";
|
|
2353
|
-
import SSHConfig from "ssh-config";
|
|
2354
|
-
var DEFAULT_SSH_KEYS = [
|
|
2355
|
-
"~/.ssh/id_rsa",
|
|
2356
|
-
"~/.ssh/id_ed25519",
|
|
2357
|
-
"~/.ssh/id_ecdsa",
|
|
2358
|
-
"~/.ssh/id_dsa"
|
|
2359
|
-
];
|
|
2360
|
-
function expandTilde(filePath) {
|
|
2361
|
-
if (filePath.startsWith("~/")) {
|
|
2362
|
-
return join(homedir(), filePath.substring(2));
|
|
2363
|
-
}
|
|
2364
|
-
return filePath;
|
|
2365
|
-
}
|
|
2366
|
-
function fileExists(filePath) {
|
|
2367
|
-
try {
|
|
2368
|
-
return existsSync(expandTilde(filePath));
|
|
2369
|
-
} catch {
|
|
2370
|
-
return false;
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
function findDefaultSSHKey() {
|
|
2374
|
-
for (const keyPath of DEFAULT_SSH_KEYS) {
|
|
2375
|
-
if (fileExists(keyPath)) {
|
|
2376
|
-
return expandTilde(keyPath);
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
return void 0;
|
|
2380
|
-
}
|
|
2381
|
-
function parseSSHConfig(hostAlias, configPath) {
|
|
2382
|
-
const sshConfigPath = configPath;
|
|
2383
|
-
if (!existsSync(sshConfigPath)) {
|
|
2384
|
-
return null;
|
|
2385
|
-
}
|
|
2386
|
-
try {
|
|
2387
|
-
const configContent = readFileSync2(sshConfigPath, "utf8");
|
|
2388
|
-
const config = SSHConfig.parse(configContent);
|
|
2389
|
-
const hostConfig = config.compute(hostAlias);
|
|
2390
|
-
if (!hostConfig || !hostConfig.HostName && !hostConfig.User) {
|
|
2391
|
-
return null;
|
|
2392
|
-
}
|
|
2393
|
-
const sshConfig = {};
|
|
2394
|
-
if (hostConfig.HostName) {
|
|
2395
|
-
sshConfig.host = hostConfig.HostName;
|
|
2396
|
-
} else {
|
|
2397
|
-
sshConfig.host = hostAlias;
|
|
2398
|
-
}
|
|
2399
|
-
if (hostConfig.Port) {
|
|
2400
|
-
sshConfig.port = parseInt(hostConfig.Port, 10);
|
|
2401
|
-
}
|
|
2402
|
-
if (hostConfig.User) {
|
|
2403
|
-
sshConfig.username = hostConfig.User;
|
|
2404
|
-
}
|
|
2405
|
-
if (hostConfig.IdentityFile) {
|
|
2406
|
-
const identityFile = Array.isArray(hostConfig.IdentityFile) ? hostConfig.IdentityFile[0] : hostConfig.IdentityFile;
|
|
2407
|
-
const expandedPath = expandTilde(identityFile);
|
|
2408
|
-
if (fileExists(expandedPath)) {
|
|
2409
|
-
sshConfig.privateKey = expandedPath;
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
if (!sshConfig.privateKey) {
|
|
2413
|
-
const defaultKey = findDefaultSSHKey();
|
|
2414
|
-
if (defaultKey) {
|
|
2415
|
-
sshConfig.privateKey = defaultKey;
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
if (hostConfig.ProxyJump || hostConfig.ProxyCommand) {
|
|
2419
|
-
console.error("Warning: ProxyJump/ProxyCommand in SSH config is not yet supported by DBHub");
|
|
2420
|
-
}
|
|
2421
|
-
if (!sshConfig.host || !sshConfig.username) {
|
|
2422
|
-
return null;
|
|
2423
|
-
}
|
|
2424
|
-
return sshConfig;
|
|
2425
|
-
} catch (error) {
|
|
2426
|
-
console.error(`Error parsing SSH config: ${error instanceof Error ? error.message : String(error)}`);
|
|
2427
|
-
return null;
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
function looksLikeSSHAlias(host) {
|
|
2431
|
-
if (host.includes(".")) {
|
|
2432
|
-
return false;
|
|
2433
|
-
}
|
|
2434
|
-
if (/^[\d:]+$/.test(host)) {
|
|
2435
|
-
return false;
|
|
2436
|
-
}
|
|
2437
|
-
if (/^[0-9a-fA-F:]+$/.test(host) && host.includes(":")) {
|
|
2438
|
-
return false;
|
|
2439
|
-
}
|
|
2440
|
-
return true;
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
// src/config/env.ts
|
|
2444
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
2445
|
-
var __dirname = path.dirname(__filename);
|
|
2446
|
-
function parseCommandLineArgs() {
|
|
2447
|
-
const args = process.argv.slice(2);
|
|
2448
|
-
const parsedManually = {};
|
|
2449
|
-
for (let i = 0; i < args.length; i++) {
|
|
2450
|
-
const arg = args[i];
|
|
2451
|
-
if (arg.startsWith("--")) {
|
|
2452
|
-
const [key, value] = arg.substring(2).split("=");
|
|
2453
|
-
if (value) {
|
|
2454
|
-
parsedManually[key] = value;
|
|
2455
|
-
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
2456
|
-
parsedManually[key] = args[i + 1];
|
|
2457
|
-
i++;
|
|
2458
|
-
} else {
|
|
2459
|
-
parsedManually[key] = "true";
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
return parsedManually;
|
|
2464
|
-
}
|
|
2465
|
-
function loadEnvFiles() {
|
|
2466
|
-
const isDevelopment = process.env.NODE_ENV === "development" || process.argv[1]?.includes("tsx");
|
|
2467
|
-
const envFileNames = isDevelopment ? [".env.local", ".env"] : [".env"];
|
|
2468
|
-
const envPaths = [];
|
|
2469
|
-
for (const fileName of envFileNames) {
|
|
2470
|
-
envPaths.push(
|
|
2471
|
-
fileName,
|
|
2472
|
-
// Current working directory
|
|
2473
|
-
path.join(__dirname, "..", "..", fileName),
|
|
2474
|
-
// Two levels up (src/config -> src -> root)
|
|
2475
|
-
path.join(process.cwd(), fileName)
|
|
2476
|
-
// Explicit current working directory
|
|
2477
|
-
);
|
|
2478
|
-
}
|
|
2479
|
-
for (const envPath of envPaths) {
|
|
2480
|
-
console.error(`Checking for env file: ${envPath}`);
|
|
2481
|
-
if (fs.existsSync(envPath)) {
|
|
2482
|
-
dotenv.config({ path: envPath });
|
|
2483
|
-
return path.basename(envPath);
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
|
-
return null;
|
|
2487
|
-
}
|
|
2488
|
-
function isDemoMode() {
|
|
2489
|
-
const args = parseCommandLineArgs();
|
|
2490
|
-
return args.demo === "true";
|
|
2491
|
-
}
|
|
2492
|
-
function isReadOnlyMode() {
|
|
2493
|
-
const args = parseCommandLineArgs();
|
|
2494
|
-
if (args.readonly !== void 0) {
|
|
2495
|
-
return args.readonly === "true";
|
|
2496
|
-
}
|
|
2497
|
-
if (process.env.READONLY !== void 0) {
|
|
2498
|
-
return process.env.READONLY === "true";
|
|
2499
|
-
}
|
|
2500
|
-
return false;
|
|
2501
|
-
}
|
|
2502
|
-
function buildDSNFromEnvParams() {
|
|
2503
|
-
const dbType = process.env.DB_TYPE;
|
|
2504
|
-
const dbHost = process.env.DB_HOST;
|
|
2505
|
-
const dbUser = process.env.DB_USER;
|
|
2506
|
-
const dbPassword = process.env.DB_PASSWORD;
|
|
2507
|
-
const dbName = process.env.DB_NAME;
|
|
2508
|
-
const dbPort = process.env.DB_PORT;
|
|
2509
|
-
if (dbType?.toLowerCase() === "sqlite") {
|
|
2510
|
-
if (!dbName) {
|
|
2511
|
-
return null;
|
|
2512
|
-
}
|
|
2513
|
-
} else {
|
|
2514
|
-
if (!dbType || !dbHost || !dbUser || !dbPassword || !dbName) {
|
|
2515
|
-
return null;
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
const supportedTypes = ["postgres", "postgresql", "mysql", "mariadb", "sqlserver", "sqlite"];
|
|
2519
|
-
if (!supportedTypes.includes(dbType.toLowerCase())) {
|
|
2520
|
-
throw new Error(`Unsupported DB_TYPE: ${dbType}. Supported types: ${supportedTypes.join(", ")}`);
|
|
2521
|
-
}
|
|
2522
|
-
let port = dbPort;
|
|
2523
|
-
if (!port) {
|
|
2524
|
-
switch (dbType.toLowerCase()) {
|
|
2525
|
-
case "postgres":
|
|
2526
|
-
case "postgresql":
|
|
2527
|
-
port = "5432";
|
|
2528
|
-
break;
|
|
2529
|
-
case "mysql":
|
|
2530
|
-
case "mariadb":
|
|
2531
|
-
port = "3306";
|
|
2532
|
-
break;
|
|
2533
|
-
case "sqlserver":
|
|
2534
|
-
port = "1433";
|
|
2535
|
-
break;
|
|
2536
|
-
case "sqlite":
|
|
2537
|
-
return {
|
|
2538
|
-
dsn: `sqlite:///${dbName}`,
|
|
2539
|
-
source: "individual environment variables"
|
|
2540
|
-
};
|
|
2541
|
-
default:
|
|
2542
|
-
throw new Error(`Unknown database type for port determination: ${dbType}`);
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
const user = dbUser;
|
|
2546
|
-
const password = dbPassword;
|
|
2547
|
-
const dbNameStr = dbName;
|
|
2548
|
-
const encodedUser = encodeURIComponent(user);
|
|
2549
|
-
const encodedPassword = encodeURIComponent(password);
|
|
2550
|
-
const encodedDbName = encodeURIComponent(dbNameStr);
|
|
2551
|
-
const protocol = dbType.toLowerCase() === "postgresql" ? "postgres" : dbType.toLowerCase();
|
|
2552
|
-
const dsn = `${protocol}://${encodedUser}:${encodedPassword}@${dbHost}:${port}/${encodedDbName}`;
|
|
2553
|
-
return {
|
|
2554
|
-
dsn,
|
|
2555
|
-
source: "individual environment variables"
|
|
2556
|
-
};
|
|
2557
|
-
}
|
|
2558
|
-
function resolveDSN() {
|
|
2559
|
-
const args = parseCommandLineArgs();
|
|
2560
|
-
if (isDemoMode()) {
|
|
2561
|
-
return {
|
|
2562
|
-
dsn: "sqlite:///:memory:",
|
|
2563
|
-
source: "demo mode",
|
|
2564
|
-
isDemo: true
|
|
2565
|
-
};
|
|
2566
|
-
}
|
|
2567
|
-
if (args.dsn) {
|
|
2568
|
-
return { dsn: args.dsn, source: "command line argument" };
|
|
2569
|
-
}
|
|
2570
|
-
if (process.env.DSN) {
|
|
2571
|
-
return { dsn: process.env.DSN, source: "environment variable" };
|
|
2572
|
-
}
|
|
2573
|
-
const envParamsResult = buildDSNFromEnvParams();
|
|
2574
|
-
if (envParamsResult) {
|
|
2575
|
-
return envParamsResult;
|
|
2576
|
-
}
|
|
2577
|
-
const loadedEnvFile = loadEnvFiles();
|
|
2578
|
-
if (loadedEnvFile && process.env.DSN) {
|
|
2579
|
-
return { dsn: process.env.DSN, source: `${loadedEnvFile} file` };
|
|
2580
|
-
}
|
|
2581
|
-
if (loadedEnvFile) {
|
|
2582
|
-
const envFileParamsResult = buildDSNFromEnvParams();
|
|
2583
|
-
if (envFileParamsResult) {
|
|
2584
|
-
return {
|
|
2585
|
-
dsn: envFileParamsResult.dsn,
|
|
2586
|
-
source: `${loadedEnvFile} file (individual parameters)`
|
|
2587
|
-
};
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
return null;
|
|
2591
|
-
}
|
|
2592
|
-
function resolveTransport() {
|
|
2593
|
-
const args = parseCommandLineArgs();
|
|
2594
|
-
if (args.transport) {
|
|
2595
|
-
const type = args.transport === "http" ? "http" : "stdio";
|
|
2596
|
-
return { type, source: "command line argument" };
|
|
2597
|
-
}
|
|
2598
|
-
if (process.env.TRANSPORT) {
|
|
2599
|
-
const type = process.env.TRANSPORT === "http" ? "http" : "stdio";
|
|
2600
|
-
return { type, source: "environment variable" };
|
|
2601
|
-
}
|
|
2602
|
-
return { type: "stdio", source: "default" };
|
|
2603
|
-
}
|
|
2604
|
-
function resolveMaxRows() {
|
|
2605
|
-
const args = parseCommandLineArgs();
|
|
2606
|
-
if (args["max-rows"]) {
|
|
2607
|
-
const maxRows = parseInt(args["max-rows"], 10);
|
|
2608
|
-
if (isNaN(maxRows) || maxRows <= 0) {
|
|
2609
|
-
throw new Error(`Invalid --max-rows value: ${args["max-rows"]}. Must be a positive integer.`);
|
|
2610
|
-
}
|
|
2611
|
-
return { maxRows, source: "command line argument" };
|
|
2612
|
-
}
|
|
2613
|
-
return null;
|
|
2614
|
-
}
|
|
2615
|
-
function resolvePort() {
|
|
2616
|
-
const args = parseCommandLineArgs();
|
|
2617
|
-
if (args.port) {
|
|
2618
|
-
const port = parseInt(args.port, 10);
|
|
2619
|
-
return { port, source: "command line argument" };
|
|
2620
|
-
}
|
|
2621
|
-
if (process.env.PORT) {
|
|
2622
|
-
const port = parseInt(process.env.PORT, 10);
|
|
2623
|
-
return { port, source: "environment variable" };
|
|
2624
|
-
}
|
|
2625
|
-
return { port: 8080, source: "default" };
|
|
2626
|
-
}
|
|
2627
|
-
function redactDSN(dsn) {
|
|
2628
|
-
try {
|
|
2629
|
-
const url = new URL(dsn);
|
|
2630
|
-
if (url.password) {
|
|
2631
|
-
url.password = "*******";
|
|
2632
|
-
}
|
|
2633
|
-
return url.toString();
|
|
2634
|
-
} catch (error) {
|
|
2635
|
-
return dsn.replace(/\/\/([^:]+):([^@]+)@/, "//$1:***@");
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
function resolveId() {
|
|
2639
|
-
const args = parseCommandLineArgs();
|
|
2640
|
-
if (args.id) {
|
|
2641
|
-
return { id: args.id, source: "command line argument" };
|
|
2642
|
-
}
|
|
2643
|
-
if (process.env.ID) {
|
|
2644
|
-
return { id: process.env.ID, source: "environment variable" };
|
|
2645
|
-
}
|
|
2646
|
-
return null;
|
|
2647
|
-
}
|
|
2648
|
-
function resolveSSHConfig() {
|
|
2649
|
-
const args = parseCommandLineArgs();
|
|
2650
|
-
const hasSSHArgs = args["ssh-host"] || process.env.SSH_HOST;
|
|
2651
|
-
if (!hasSSHArgs) {
|
|
2652
|
-
return null;
|
|
2653
|
-
}
|
|
2654
|
-
let config = {};
|
|
2655
|
-
let sources = [];
|
|
2656
|
-
let sshConfigHost;
|
|
2657
|
-
if (args["ssh-host"]) {
|
|
2658
|
-
sshConfigHost = args["ssh-host"];
|
|
2659
|
-
config.host = args["ssh-host"];
|
|
2660
|
-
sources.push("ssh-host from command line");
|
|
2661
|
-
} else if (process.env.SSH_HOST) {
|
|
2662
|
-
sshConfigHost = process.env.SSH_HOST;
|
|
2663
|
-
config.host = process.env.SSH_HOST;
|
|
2664
|
-
sources.push("SSH_HOST from environment");
|
|
2665
|
-
}
|
|
2666
|
-
if (sshConfigHost && looksLikeSSHAlias(sshConfigHost)) {
|
|
2667
|
-
const sshConfigPath = path.join(homedir2(), ".ssh", "config");
|
|
2668
|
-
console.error(`Attempting to parse SSH config for host '${sshConfigHost}' from: ${sshConfigPath}`);
|
|
2669
|
-
const sshConfigData = parseSSHConfig(sshConfigHost, sshConfigPath);
|
|
2670
|
-
if (sshConfigData) {
|
|
2671
|
-
config = { ...sshConfigData };
|
|
2672
|
-
sources.push(`SSH config for host '${sshConfigHost}'`);
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
if (args["ssh-port"]) {
|
|
2676
|
-
config.port = parseInt(args["ssh-port"], 10);
|
|
2677
|
-
sources.push("ssh-port from command line");
|
|
2678
|
-
} else if (process.env.SSH_PORT) {
|
|
2679
|
-
config.port = parseInt(process.env.SSH_PORT, 10);
|
|
2680
|
-
sources.push("SSH_PORT from environment");
|
|
2681
|
-
}
|
|
2682
|
-
if (args["ssh-user"]) {
|
|
2683
|
-
config.username = args["ssh-user"];
|
|
2684
|
-
sources.push("ssh-user from command line");
|
|
2685
|
-
} else if (process.env.SSH_USER) {
|
|
2686
|
-
config.username = process.env.SSH_USER;
|
|
2687
|
-
sources.push("SSH_USER from environment");
|
|
2688
|
-
}
|
|
2689
|
-
if (args["ssh-password"]) {
|
|
2690
|
-
config.password = args["ssh-password"];
|
|
2691
|
-
sources.push("ssh-password from command line");
|
|
2692
|
-
} else if (process.env.SSH_PASSWORD) {
|
|
2693
|
-
config.password = process.env.SSH_PASSWORD;
|
|
2694
|
-
sources.push("SSH_PASSWORD from environment");
|
|
2695
|
-
}
|
|
2696
|
-
if (args["ssh-key"]) {
|
|
2697
|
-
config.privateKey = args["ssh-key"];
|
|
2698
|
-
if (config.privateKey.startsWith("~/")) {
|
|
2699
|
-
config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
|
|
2700
|
-
}
|
|
2701
|
-
sources.push("ssh-key from command line");
|
|
2702
|
-
} else if (process.env.SSH_KEY) {
|
|
2703
|
-
config.privateKey = process.env.SSH_KEY;
|
|
2704
|
-
if (config.privateKey.startsWith("~/")) {
|
|
2705
|
-
config.privateKey = path.join(process.env.HOME || "", config.privateKey.substring(2));
|
|
2706
|
-
}
|
|
2707
|
-
sources.push("SSH_KEY from environment");
|
|
2708
|
-
}
|
|
2709
|
-
if (args["ssh-passphrase"]) {
|
|
2710
|
-
config.passphrase = args["ssh-passphrase"];
|
|
2711
|
-
sources.push("ssh-passphrase from command line");
|
|
2712
|
-
} else if (process.env.SSH_PASSPHRASE) {
|
|
2713
|
-
config.passphrase = process.env.SSH_PASSPHRASE;
|
|
2714
|
-
sources.push("SSH_PASSPHRASE from environment");
|
|
2715
|
-
}
|
|
2716
|
-
if (!config.host || !config.username) {
|
|
2717
|
-
throw new Error("SSH tunnel configuration requires at least --ssh-host and --ssh-user");
|
|
2718
|
-
}
|
|
2719
|
-
if (!config.password && !config.privateKey) {
|
|
2720
|
-
throw new Error("SSH tunnel configuration requires either --ssh-password or --ssh-key for authentication");
|
|
2721
|
-
}
|
|
2722
|
-
return {
|
|
2723
|
-
config,
|
|
2724
|
-
source: sources.join(", ")
|
|
2725
|
-
};
|
|
2726
|
-
}
|
|
2727
|
-
async function resolveSourceConfigs() {
|
|
2728
|
-
if (!isDemoMode()) {
|
|
2729
|
-
const tomlConfig = loadTomlConfig();
|
|
2730
|
-
if (tomlConfig) {
|
|
2731
|
-
const idData = resolveId();
|
|
2732
|
-
if (idData) {
|
|
2733
|
-
throw new Error(
|
|
2734
|
-
"The --id flag cannot be used with TOML configuration. TOML config defines source IDs directly. Either remove the --id flag or use command-line DSN configuration instead."
|
|
2735
|
-
);
|
|
2736
|
-
}
|
|
2737
|
-
if (isReadOnlyMode()) {
|
|
2738
|
-
throw new Error(
|
|
2739
|
-
"The --readonly flag cannot be used with TOML configuration. TOML config defines readonly mode per-source using 'readonly = true'. Either remove the --readonly flag or use command-line DSN configuration instead."
|
|
2740
|
-
);
|
|
2741
|
-
}
|
|
2742
|
-
return tomlConfig;
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
const dsnResult = resolveDSN();
|
|
2746
|
-
if (dsnResult) {
|
|
2747
|
-
let dsnUrl;
|
|
2748
|
-
try {
|
|
2749
|
-
dsnUrl = new URL(dsnResult.dsn);
|
|
2750
|
-
} catch (error) {
|
|
2751
|
-
throw new Error(
|
|
2752
|
-
`Invalid DSN format: ${dsnResult.dsn}. Expected format: protocol://[user[:password]@]host[:port]/database`
|
|
2753
|
-
);
|
|
2754
|
-
}
|
|
2755
|
-
const protocol = dsnUrl.protocol.replace(":", "");
|
|
2756
|
-
let dbType;
|
|
2757
|
-
if (protocol === "postgresql" || protocol === "postgres") {
|
|
2758
|
-
dbType = "postgres";
|
|
2759
|
-
} else if (protocol === "mysql") {
|
|
2760
|
-
dbType = "mysql";
|
|
2761
|
-
} else if (protocol === "mariadb") {
|
|
2762
|
-
dbType = "mariadb";
|
|
2763
|
-
} else if (protocol === "sqlserver") {
|
|
2764
|
-
dbType = "sqlserver";
|
|
2765
|
-
} else if (protocol === "sqlite") {
|
|
2766
|
-
dbType = "sqlite";
|
|
2767
|
-
} else {
|
|
2768
|
-
throw new Error(`Unsupported database type in DSN: ${protocol}`);
|
|
2769
|
-
}
|
|
2770
|
-
const idData = resolveId();
|
|
2771
|
-
const sourceId = idData?.id || "default";
|
|
2772
|
-
const source = {
|
|
2773
|
-
id: sourceId,
|
|
2774
|
-
type: dbType,
|
|
2775
|
-
dsn: dsnResult.dsn
|
|
2776
|
-
};
|
|
2777
|
-
const connectionInfo = parseConnectionInfoFromDSN(dsnResult.dsn);
|
|
2778
|
-
if (connectionInfo) {
|
|
2779
|
-
if (connectionInfo.host) {
|
|
2780
|
-
source.host = connectionInfo.host;
|
|
2781
|
-
}
|
|
2782
|
-
if (connectionInfo.port !== void 0) {
|
|
2783
|
-
source.port = connectionInfo.port;
|
|
2784
|
-
}
|
|
2785
|
-
if (connectionInfo.database) {
|
|
2786
|
-
source.database = connectionInfo.database;
|
|
2787
|
-
}
|
|
2788
|
-
if (connectionInfo.user) {
|
|
2789
|
-
source.user = connectionInfo.user;
|
|
2790
|
-
}
|
|
2791
|
-
}
|
|
2792
|
-
const sshResult = resolveSSHConfig();
|
|
2793
|
-
if (sshResult) {
|
|
2794
|
-
source.ssh_host = sshResult.config.host;
|
|
2795
|
-
source.ssh_port = sshResult.config.port;
|
|
2796
|
-
source.ssh_user = sshResult.config.username;
|
|
2797
|
-
source.ssh_password = sshResult.config.password;
|
|
2798
|
-
source.ssh_key = sshResult.config.privateKey;
|
|
2799
|
-
source.ssh_passphrase = sshResult.config.passphrase;
|
|
2800
|
-
}
|
|
2801
|
-
source.readonly = isReadOnlyMode();
|
|
2802
|
-
const maxRowsResult = resolveMaxRows();
|
|
2803
|
-
if (maxRowsResult) {
|
|
2804
|
-
source.max_rows = maxRowsResult.maxRows;
|
|
2805
|
-
}
|
|
2806
|
-
if (dsnResult.isDemo) {
|
|
2807
|
-
const { getSqliteInMemorySetupSql } = await import("./demo-loader-PSMTLZ2T.js");
|
|
2808
|
-
source.init_script = getSqliteInMemorySetupSql();
|
|
2809
|
-
}
|
|
2810
|
-
return {
|
|
2811
|
-
sources: [source],
|
|
2812
|
-
source: dsnResult.isDemo ? "demo mode" : dsnResult.source
|
|
2813
|
-
};
|
|
2814
|
-
}
|
|
2815
|
-
return null;
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
|
-
// src/config/toml-loader.ts
|
|
2819
|
-
function loadTomlConfig() {
|
|
2820
|
-
const configPath = resolveTomlConfigPath();
|
|
2821
|
-
if (!configPath) {
|
|
2822
|
-
return null;
|
|
2823
|
-
}
|
|
2824
|
-
try {
|
|
2825
|
-
const fileContent = fs2.readFileSync(configPath, "utf-8");
|
|
2826
|
-
const parsedToml = toml.parse(fileContent);
|
|
2827
|
-
validateTomlConfig(parsedToml, configPath);
|
|
2828
|
-
const sources = processSourceConfigs(parsedToml.sources, configPath);
|
|
2829
|
-
return {
|
|
2830
|
-
sources,
|
|
2831
|
-
source: path2.basename(configPath)
|
|
2832
|
-
};
|
|
2833
|
-
} catch (error) {
|
|
2834
|
-
if (error instanceof Error) {
|
|
2835
|
-
throw new Error(
|
|
2836
|
-
`Failed to load TOML configuration from ${configPath}: ${error.message}`
|
|
2837
|
-
);
|
|
2838
|
-
}
|
|
2839
|
-
throw error;
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
function resolveTomlConfigPath() {
|
|
2843
|
-
const args = parseCommandLineArgs();
|
|
2844
|
-
if (args.config) {
|
|
2845
|
-
const configPath = expandHomeDir(args.config);
|
|
2846
|
-
if (!fs2.existsSync(configPath)) {
|
|
2847
|
-
throw new Error(
|
|
2848
|
-
`Configuration file specified by --config flag not found: ${configPath}`
|
|
2849
|
-
);
|
|
2850
|
-
}
|
|
2851
|
-
return configPath;
|
|
2852
|
-
}
|
|
2853
|
-
const defaultConfigPath = path2.join(process.cwd(), "dbhub.toml");
|
|
2854
|
-
if (fs2.existsSync(defaultConfigPath)) {
|
|
2855
|
-
return defaultConfigPath;
|
|
2856
|
-
}
|
|
2857
|
-
return null;
|
|
2858
|
-
}
|
|
2859
|
-
function validateTomlConfig(config, configPath) {
|
|
2860
|
-
if (!config.sources) {
|
|
2861
|
-
throw new Error(
|
|
2862
|
-
`Configuration file ${configPath} must contain a [[sources]] array. Example:
|
|
2863
|
-
|
|
2864
|
-
[[sources]]
|
|
2865
|
-
id = "my_db"
|
|
2866
|
-
dsn = "postgres://..."`
|
|
2867
|
-
);
|
|
2868
|
-
}
|
|
2869
|
-
if (!Array.isArray(config.sources)) {
|
|
2870
|
-
throw new Error(
|
|
2871
|
-
`Configuration file ${configPath}: 'sources' must be an array. Use [[sources]] syntax for array of tables in TOML.`
|
|
2872
|
-
);
|
|
2873
|
-
}
|
|
2874
|
-
if (config.sources.length === 0) {
|
|
2875
|
-
throw new Error(
|
|
2876
|
-
`Configuration file ${configPath}: sources array cannot be empty. Please define at least one source with [[sources]].`
|
|
2877
|
-
);
|
|
2878
|
-
}
|
|
2879
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2880
|
-
const duplicates = [];
|
|
2881
|
-
for (const source of config.sources) {
|
|
2882
|
-
if (!source.id) {
|
|
2883
|
-
throw new Error(
|
|
2884
|
-
`Configuration file ${configPath}: each source must have an 'id' field. Example: [[sources]]
|
|
2885
|
-
id = "my_db"`
|
|
2886
|
-
);
|
|
2887
|
-
}
|
|
2888
|
-
if (ids.has(source.id)) {
|
|
2889
|
-
duplicates.push(source.id);
|
|
2890
|
-
} else {
|
|
2891
|
-
ids.add(source.id);
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
if (duplicates.length > 0) {
|
|
2895
|
-
throw new Error(
|
|
2896
|
-
`Configuration file ${configPath}: duplicate source IDs found: ${duplicates.join(", ")}. Each source must have a unique 'id' field.`
|
|
2897
|
-
);
|
|
2898
|
-
}
|
|
2899
|
-
for (const source of config.sources) {
|
|
2900
|
-
validateSourceConfig(source, configPath);
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
function validateSourceConfig(source, configPath) {
|
|
2904
|
-
const hasConnectionParams = source.type && (source.type === "sqlite" ? source.database : source.host);
|
|
2905
|
-
if (!source.dsn && !hasConnectionParams) {
|
|
2906
|
-
throw new Error(
|
|
2907
|
-
`Configuration file ${configPath}: source '${source.id}' must have either:
|
|
2908
|
-
- 'dsn' field (e.g., dsn = "postgres://user:pass@host:5432/dbname")
|
|
2909
|
-
- OR connection parameters (type, host, database, user, password)
|
|
2910
|
-
- For SQLite: type = "sqlite" and database path`
|
|
2911
|
-
);
|
|
2912
|
-
}
|
|
2913
|
-
if (source.type) {
|
|
2914
|
-
const validTypes = ["postgres", "mysql", "mariadb", "sqlserver", "sqlite"];
|
|
2915
|
-
if (!validTypes.includes(source.type)) {
|
|
2916
|
-
throw new Error(
|
|
2917
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid type '${source.type}'. Valid types: ${validTypes.join(", ")}`
|
|
2918
|
-
);
|
|
2919
|
-
}
|
|
2920
|
-
}
|
|
2921
|
-
if (source.max_rows !== void 0) {
|
|
2922
|
-
if (typeof source.max_rows !== "number" || source.max_rows <= 0) {
|
|
2923
|
-
throw new Error(
|
|
2924
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid max_rows. Must be a positive integer.`
|
|
2925
|
-
);
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
if (source.connection_timeout !== void 0) {
|
|
2929
|
-
if (typeof source.connection_timeout !== "number" || source.connection_timeout <= 0) {
|
|
2930
|
-
throw new Error(
|
|
2931
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid connection_timeout. Must be a positive number (in seconds).`
|
|
2932
|
-
);
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
if (source.request_timeout !== void 0) {
|
|
2936
|
-
if (typeof source.request_timeout !== "number" || source.request_timeout <= 0) {
|
|
2937
|
-
throw new Error(
|
|
2938
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid request_timeout. Must be a positive number (in seconds).`
|
|
2939
|
-
);
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
if (source.ssh_port !== void 0) {
|
|
2943
|
-
if (typeof source.ssh_port !== "number" || source.ssh_port <= 0 || source.ssh_port > 65535) {
|
|
2944
|
-
throw new Error(
|
|
2945
|
-
`Configuration file ${configPath}: source '${source.id}' has invalid ssh_port. Must be between 1 and 65535.`
|
|
2946
|
-
);
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
function processSourceConfigs(sources, configPath) {
|
|
2951
|
-
return sources.map((source) => {
|
|
2952
|
-
const processed = { ...source };
|
|
2953
|
-
if (processed.ssh_key) {
|
|
2954
|
-
processed.ssh_key = expandHomeDir(processed.ssh_key);
|
|
2955
|
-
}
|
|
2956
|
-
if (processed.type === "sqlite" && processed.database) {
|
|
2957
|
-
processed.database = expandHomeDir(processed.database);
|
|
2958
|
-
}
|
|
2959
|
-
if (processed.dsn && processed.dsn.startsWith("sqlite:///~")) {
|
|
2960
|
-
processed.dsn = `sqlite:///${expandHomeDir(processed.dsn.substring(11))}`;
|
|
2961
|
-
}
|
|
2962
|
-
if (processed.dsn) {
|
|
2963
|
-
const connectionInfo = parseConnectionInfoFromDSN(processed.dsn);
|
|
2964
|
-
if (connectionInfo) {
|
|
2965
|
-
if (!processed.type && connectionInfo.type) {
|
|
2966
|
-
processed.type = connectionInfo.type;
|
|
2967
|
-
}
|
|
2968
|
-
if (!processed.host && connectionInfo.host) {
|
|
2969
|
-
processed.host = connectionInfo.host;
|
|
2970
|
-
}
|
|
2971
|
-
if (processed.port === void 0 && connectionInfo.port !== void 0) {
|
|
2972
|
-
processed.port = connectionInfo.port;
|
|
2973
|
-
}
|
|
2974
|
-
if (!processed.database && connectionInfo.database) {
|
|
2975
|
-
processed.database = connectionInfo.database;
|
|
2976
|
-
}
|
|
2977
|
-
if (!processed.user && connectionInfo.user) {
|
|
2978
|
-
processed.user = connectionInfo.user;
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
}
|
|
2982
|
-
return processed;
|
|
2983
|
-
});
|
|
2984
|
-
}
|
|
2985
|
-
function expandHomeDir(filePath) {
|
|
2986
|
-
if (filePath.startsWith("~/")) {
|
|
2987
|
-
return path2.join(homedir3(), filePath.substring(2));
|
|
2988
|
-
}
|
|
2989
|
-
return filePath;
|
|
2990
|
-
}
|
|
2991
|
-
function buildDSNFromSource(source) {
|
|
2992
|
-
if (source.dsn) {
|
|
2993
|
-
return source.dsn;
|
|
2994
|
-
}
|
|
2995
|
-
if (!source.type) {
|
|
2996
|
-
throw new Error(
|
|
2997
|
-
`Source '${source.id}': 'type' field is required when 'dsn' is not provided`
|
|
2998
|
-
);
|
|
2999
|
-
}
|
|
3000
|
-
if (source.type === "sqlite") {
|
|
3001
|
-
if (!source.database) {
|
|
3002
|
-
throw new Error(
|
|
3003
|
-
`Source '${source.id}': 'database' field is required for SQLite`
|
|
3004
|
-
);
|
|
3005
|
-
}
|
|
3006
|
-
return `sqlite:///${source.database}`;
|
|
3007
|
-
}
|
|
3008
|
-
if (!source.host || !source.user || !source.password || !source.database) {
|
|
3009
|
-
throw new Error(
|
|
3010
|
-
`Source '${source.id}': missing required connection parameters. Required: type, host, user, password, database`
|
|
3011
|
-
);
|
|
3012
|
-
}
|
|
3013
|
-
const port = source.port || getDefaultPortForType(source.type);
|
|
3014
|
-
if (!port) {
|
|
3015
|
-
throw new Error(`Source '${source.id}': unable to determine port`);
|
|
3016
|
-
}
|
|
3017
|
-
const encodedUser = encodeURIComponent(source.user);
|
|
3018
|
-
const encodedPassword = encodeURIComponent(source.password);
|
|
3019
|
-
const encodedDatabase = encodeURIComponent(source.database);
|
|
3020
|
-
let dsn = `${source.type}://${encodedUser}:${encodedPassword}@${source.host}:${port}/${encodedDatabase}`;
|
|
3021
|
-
if (source.type === "sqlserver" && source.instanceName) {
|
|
3022
|
-
dsn += `?instanceName=${encodeURIComponent(source.instanceName)}`;
|
|
3023
|
-
}
|
|
3024
|
-
return dsn;
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
// src/connectors/manager.ts
|
|
3028
|
-
var managerInstance = null;
|
|
3029
|
-
var ConnectorManager = class {
|
|
3030
|
-
// Ordered list of source IDs (first is default)
|
|
3031
|
-
constructor() {
|
|
3032
|
-
// Maps for multi-source support
|
|
3033
|
-
this.connectors = /* @__PURE__ */ new Map();
|
|
3034
|
-
this.sshTunnels = /* @__PURE__ */ new Map();
|
|
3035
|
-
this.executeOptions = /* @__PURE__ */ new Map();
|
|
3036
|
-
this.sourceConfigs = /* @__PURE__ */ new Map();
|
|
3037
|
-
// Store original source configs
|
|
3038
|
-
this.sourceIds = [];
|
|
3039
|
-
if (!managerInstance) {
|
|
3040
|
-
managerInstance = this;
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
/**
|
|
3044
|
-
* Initialize and connect to multiple databases using source configurations
|
|
3045
|
-
* This is the new multi-source connection method
|
|
3046
|
-
*/
|
|
3047
|
-
async connectWithSources(sources) {
|
|
3048
|
-
if (sources.length === 0) {
|
|
3049
|
-
throw new Error("No sources provided");
|
|
3050
|
-
}
|
|
3051
|
-
for (const source of sources) {
|
|
3052
|
-
await this.connectSource(source);
|
|
3053
|
-
}
|
|
3054
|
-
console.error(`Successfully connected to ${sources.length} database source(s)`);
|
|
3055
|
-
}
|
|
3056
|
-
/**
|
|
3057
|
-
* Connect to a single source (helper for connectWithSources)
|
|
3058
|
-
*/
|
|
3059
|
-
async connectSource(source) {
|
|
3060
|
-
const sourceId = source.id;
|
|
3061
|
-
console.error(`Connecting to source '${sourceId || "(default)"}' ...`);
|
|
3062
|
-
const dsn = buildDSNFromSource(source);
|
|
3063
|
-
let actualDSN = dsn;
|
|
3064
|
-
if (source.ssh_host) {
|
|
3065
|
-
if (!source.ssh_user) {
|
|
3066
|
-
throw new Error(
|
|
3067
|
-
`Source '${sourceId}': SSH tunnel requires ssh_user`
|
|
3068
|
-
);
|
|
3069
|
-
}
|
|
3070
|
-
const sshConfig = {
|
|
3071
|
-
host: source.ssh_host,
|
|
3072
|
-
port: source.ssh_port || 22,
|
|
3073
|
-
username: source.ssh_user,
|
|
3074
|
-
password: source.ssh_password,
|
|
3075
|
-
privateKey: source.ssh_key,
|
|
3076
|
-
passphrase: source.ssh_passphrase
|
|
3077
|
-
};
|
|
3078
|
-
if (!sshConfig.password && !sshConfig.privateKey) {
|
|
3079
|
-
throw new Error(
|
|
3080
|
-
`Source '${sourceId}': SSH tunnel requires either ssh_password or ssh_key`
|
|
3081
|
-
);
|
|
3082
|
-
}
|
|
3083
|
-
const url = new URL(dsn);
|
|
3084
|
-
const targetHost = url.hostname;
|
|
3085
|
-
const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
|
|
3086
|
-
const tunnel = new SSHTunnel();
|
|
3087
|
-
const tunnelInfo = await tunnel.establish(sshConfig, {
|
|
3088
|
-
targetHost,
|
|
3089
|
-
targetPort
|
|
3090
|
-
});
|
|
3091
|
-
url.hostname = "127.0.0.1";
|
|
3092
|
-
url.port = tunnelInfo.localPort.toString();
|
|
3093
|
-
actualDSN = url.toString();
|
|
3094
|
-
this.sshTunnels.set(sourceId, tunnel);
|
|
3095
|
-
console.error(
|
|
3096
|
-
` SSH tunnel established through localhost:${tunnelInfo.localPort}`
|
|
3097
|
-
);
|
|
3098
|
-
}
|
|
3099
|
-
const connectorPrototype = ConnectorRegistry.getConnectorForDSN(actualDSN);
|
|
3100
|
-
if (!connectorPrototype) {
|
|
3101
|
-
throw new Error(
|
|
3102
|
-
`Source '${sourceId}': No connector found for DSN: ${actualDSN}`
|
|
3103
|
-
);
|
|
3104
|
-
}
|
|
3105
|
-
const connector = connectorPrototype.clone();
|
|
3106
|
-
const config = {};
|
|
3107
|
-
if (source.connection_timeout !== void 0) {
|
|
3108
|
-
config.connectionTimeoutSeconds = source.connection_timeout;
|
|
3109
|
-
}
|
|
3110
|
-
if (connector.id === "sqlserver" && source.request_timeout !== void 0) {
|
|
3111
|
-
config.requestTimeoutSeconds = source.request_timeout;
|
|
3112
|
-
}
|
|
3113
|
-
if (source.readonly !== void 0) {
|
|
3114
|
-
config.readonly = source.readonly;
|
|
3115
|
-
}
|
|
3116
|
-
await connector.connect(actualDSN, source.init_script, config);
|
|
3117
|
-
this.connectors.set(sourceId, connector);
|
|
3118
|
-
this.sourceIds.push(sourceId);
|
|
3119
|
-
this.sourceConfigs.set(sourceId, source);
|
|
3120
|
-
const options = {};
|
|
3121
|
-
if (source.max_rows !== void 0) {
|
|
3122
|
-
options.maxRows = source.max_rows;
|
|
3123
|
-
}
|
|
3124
|
-
if (source.readonly !== void 0) {
|
|
3125
|
-
options.readonly = source.readonly;
|
|
3126
|
-
}
|
|
3127
|
-
this.executeOptions.set(sourceId, options);
|
|
3128
|
-
console.error(` Connected successfully`);
|
|
3129
|
-
}
|
|
3130
|
-
/**
|
|
3131
|
-
* Close all database connections
|
|
3132
|
-
*/
|
|
3133
|
-
async disconnect() {
|
|
3134
|
-
for (const [sourceId, connector] of this.connectors.entries()) {
|
|
3135
|
-
try {
|
|
3136
|
-
await connector.disconnect();
|
|
3137
|
-
console.error(`Disconnected from source '${sourceId || "(default)"}'`);
|
|
3138
|
-
} catch (error) {
|
|
3139
|
-
console.error(`Error disconnecting from source '${sourceId}':`, error);
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
for (const [sourceId, tunnel] of this.sshTunnels.entries()) {
|
|
3143
|
-
try {
|
|
3144
|
-
await tunnel.close();
|
|
3145
|
-
} catch (error) {
|
|
3146
|
-
console.error(`Error closing SSH tunnel for source '${sourceId}':`, error);
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
this.connectors.clear();
|
|
3150
|
-
this.sshTunnels.clear();
|
|
3151
|
-
this.executeOptions.clear();
|
|
3152
|
-
this.sourceConfigs.clear();
|
|
3153
|
-
this.sourceIds = [];
|
|
3154
|
-
}
|
|
3155
|
-
/**
|
|
3156
|
-
* Get a connector by source ID
|
|
3157
|
-
* If sourceId is not provided, returns the default (first) connector
|
|
3158
|
-
*/
|
|
3159
|
-
getConnector(sourceId) {
|
|
3160
|
-
const id = sourceId || this.sourceIds[0];
|
|
3161
|
-
const connector = this.connectors.get(id);
|
|
3162
|
-
if (!connector) {
|
|
3163
|
-
if (sourceId) {
|
|
3164
|
-
throw new Error(
|
|
3165
|
-
`Source '${sourceId}' not found. Available sources: ${this.sourceIds.join(", ")}`
|
|
3166
|
-
);
|
|
3167
|
-
} else {
|
|
3168
|
-
throw new Error("No sources connected. Call connectWithSources() first.");
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
return connector;
|
|
3172
|
-
}
|
|
3173
|
-
/**
|
|
3174
|
-
* Get all available connector types
|
|
3175
|
-
*/
|
|
3176
|
-
static getAvailableConnectors() {
|
|
3177
|
-
return ConnectorRegistry.getAvailableConnectors();
|
|
3178
|
-
}
|
|
3179
|
-
/**
|
|
3180
|
-
* Get sample DSNs for all available connectors
|
|
3181
|
-
*/
|
|
3182
|
-
static getAllSampleDSNs() {
|
|
3183
|
-
return ConnectorRegistry.getAllSampleDSNs();
|
|
3184
|
-
}
|
|
3185
|
-
/**
|
|
3186
|
-
* Get the current active connector instance
|
|
3187
|
-
* This is used by resource and tool handlers
|
|
3188
|
-
* @param sourceId - Optional source ID. If not provided, returns default (first) connector
|
|
3189
|
-
*/
|
|
3190
|
-
static getCurrentConnector(sourceId) {
|
|
3191
|
-
if (!managerInstance) {
|
|
3192
|
-
throw new Error("ConnectorManager not initialized");
|
|
3193
|
-
}
|
|
3194
|
-
return managerInstance.getConnector(sourceId);
|
|
3195
|
-
}
|
|
3196
|
-
/**
|
|
3197
|
-
* Get execute options for SQL execution
|
|
3198
|
-
* @param sourceId - Optional source ID. If not provided, returns default options
|
|
3199
|
-
*/
|
|
3200
|
-
getExecuteOptions(sourceId) {
|
|
3201
|
-
const id = sourceId || this.sourceIds[0];
|
|
3202
|
-
return this.executeOptions.get(id) || {};
|
|
3203
|
-
}
|
|
3204
|
-
/**
|
|
3205
|
-
* Get the current execute options
|
|
3206
|
-
* This is used by tool handlers
|
|
3207
|
-
* @param sourceId - Optional source ID. If not provided, returns default options
|
|
3208
|
-
*/
|
|
3209
|
-
static getCurrentExecuteOptions(sourceId) {
|
|
3210
|
-
if (!managerInstance) {
|
|
3211
|
-
throw new Error("ConnectorManager not initialized");
|
|
3212
|
-
}
|
|
3213
|
-
return managerInstance.getExecuteOptions(sourceId);
|
|
3214
|
-
}
|
|
3215
|
-
/**
|
|
3216
|
-
* Get all available source IDs
|
|
3217
|
-
*/
|
|
3218
|
-
getSourceIds() {
|
|
3219
|
-
return [...this.sourceIds];
|
|
3220
|
-
}
|
|
3221
|
-
/** Get all available source IDs */
|
|
3222
|
-
static getAvailableSourceIds() {
|
|
3223
|
-
if (!managerInstance) {
|
|
3224
|
-
throw new Error("ConnectorManager not initialized");
|
|
3225
|
-
}
|
|
3226
|
-
return managerInstance.getSourceIds();
|
|
3227
|
-
}
|
|
3228
|
-
/**
|
|
3229
|
-
* Get source configuration by ID
|
|
3230
|
-
* @param sourceId - Source ID. If not provided, returns default (first) source config
|
|
3231
|
-
*/
|
|
3232
|
-
getSourceConfig(sourceId) {
|
|
3233
|
-
if (this.connectors.size === 0) {
|
|
3234
|
-
return null;
|
|
3235
|
-
}
|
|
3236
|
-
const id = sourceId || this.sourceIds[0];
|
|
3237
|
-
return this.sourceConfigs.get(id) || null;
|
|
3238
|
-
}
|
|
3239
|
-
/**
|
|
3240
|
-
* Get all source configurations
|
|
3241
|
-
*/
|
|
3242
|
-
getAllSourceConfigs() {
|
|
3243
|
-
return this.sourceIds.map((id) => this.sourceConfigs.get(id));
|
|
3244
|
-
}
|
|
3245
|
-
/**
|
|
3246
|
-
* Get source configuration by ID (static method for external access)
|
|
3247
|
-
*/
|
|
3248
|
-
static getSourceConfig(sourceId) {
|
|
3249
|
-
if (!managerInstance) {
|
|
3250
|
-
throw new Error("ConnectorManager not initialized");
|
|
3251
|
-
}
|
|
3252
|
-
return managerInstance.getSourceConfig(sourceId);
|
|
3253
|
-
}
|
|
3254
|
-
/**
|
|
3255
|
-
* Get all source configurations (static method for external access)
|
|
3256
|
-
*/
|
|
3257
|
-
static getAllSourceConfigs() {
|
|
3258
|
-
if (!managerInstance) {
|
|
3259
|
-
throw new Error("ConnectorManager not initialized");
|
|
3260
|
-
}
|
|
3261
|
-
return managerInstance.getAllSourceConfigs();
|
|
3262
|
-
}
|
|
3263
|
-
/**
|
|
3264
|
-
* Get default port for a database based on DSN protocol
|
|
3265
|
-
*/
|
|
3266
|
-
getDefaultPort(dsn) {
|
|
3267
|
-
const type = getDatabaseTypeFromDSN(dsn);
|
|
3268
|
-
if (!type) {
|
|
3269
|
-
return 0;
|
|
3270
|
-
}
|
|
3271
|
-
return getDefaultPortForType(type) ?? 0;
|
|
3272
|
-
}
|
|
3273
|
-
};
|
|
3274
|
-
|
|
3275
|
-
// src/resources/index.ts
|
|
3276
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3277
|
-
|
|
3278
|
-
// src/utils/response-formatter.ts
|
|
3279
|
-
function bigIntReplacer(_key, value) {
|
|
3280
|
-
if (typeof value === "bigint") {
|
|
3281
|
-
return value.toString();
|
|
3282
|
-
}
|
|
3283
|
-
return value;
|
|
3284
|
-
}
|
|
3285
|
-
function formatSuccessResponse(data, meta = {}) {
|
|
3286
|
-
return {
|
|
3287
|
-
success: true,
|
|
3288
|
-
data,
|
|
3289
|
-
...Object.keys(meta).length > 0 ? { meta } : {}
|
|
3290
|
-
};
|
|
3291
|
-
}
|
|
3292
|
-
function formatErrorResponse(error, code = "ERROR", details) {
|
|
3293
|
-
return {
|
|
3294
|
-
success: false,
|
|
3295
|
-
error,
|
|
3296
|
-
code,
|
|
3297
|
-
...details ? { details } : {}
|
|
3298
|
-
};
|
|
3299
|
-
}
|
|
3300
|
-
function createToolErrorResponse(error, code = "ERROR", details) {
|
|
3301
|
-
return {
|
|
3302
|
-
content: [
|
|
3303
|
-
{
|
|
3304
|
-
type: "text",
|
|
3305
|
-
text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
|
|
3306
|
-
mimeType: "application/json"
|
|
3307
|
-
}
|
|
3308
|
-
],
|
|
3309
|
-
isError: true
|
|
3310
|
-
};
|
|
3311
|
-
}
|
|
3312
|
-
function createToolSuccessResponse(data, meta = {}) {
|
|
3313
|
-
return {
|
|
3314
|
-
content: [
|
|
3315
|
-
{
|
|
3316
|
-
type: "text",
|
|
3317
|
-
text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
|
|
3318
|
-
mimeType: "application/json"
|
|
3319
|
-
}
|
|
3320
|
-
]
|
|
3321
|
-
};
|
|
3322
|
-
}
|
|
3323
|
-
function createResourceErrorResponse(uri, error, code = "ERROR", details) {
|
|
3324
|
-
return {
|
|
3325
|
-
contents: [
|
|
3326
|
-
{
|
|
3327
|
-
uri,
|
|
3328
|
-
text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
|
|
3329
|
-
mimeType: "application/json"
|
|
3330
|
-
}
|
|
3331
|
-
]
|
|
3332
|
-
};
|
|
3333
|
-
}
|
|
3334
|
-
function createResourceSuccessResponse(uri, data, meta = {}) {
|
|
3335
|
-
return {
|
|
3336
|
-
contents: [
|
|
3337
|
-
{
|
|
3338
|
-
uri,
|
|
3339
|
-
text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
|
|
3340
|
-
mimeType: "application/json"
|
|
3341
|
-
}
|
|
3342
|
-
]
|
|
3343
|
-
};
|
|
3344
|
-
}
|
|
3345
|
-
function formatPromptSuccessResponse(text, references = []) {
|
|
3346
|
-
return {
|
|
3347
|
-
messages: [
|
|
3348
|
-
{
|
|
3349
|
-
role: "assistant",
|
|
3350
|
-
content: {
|
|
3351
|
-
type: "text",
|
|
3352
|
-
text
|
|
3353
|
-
}
|
|
3354
|
-
}
|
|
3355
|
-
],
|
|
3356
|
-
...references.length > 0 ? { references } : {}
|
|
3357
|
-
};
|
|
3358
|
-
}
|
|
3359
|
-
function formatPromptErrorResponse(error, code = "ERROR") {
|
|
3360
|
-
return {
|
|
3361
|
-
messages: [
|
|
3362
|
-
{
|
|
3363
|
-
role: "assistant",
|
|
3364
|
-
content: {
|
|
3365
|
-
type: "text",
|
|
3366
|
-
text: `Error: ${error}`
|
|
3367
|
-
}
|
|
3368
|
-
}
|
|
3369
|
-
],
|
|
3370
|
-
error,
|
|
3371
|
-
code
|
|
3372
|
-
};
|
|
3373
|
-
}
|
|
3374
|
-
|
|
3375
|
-
// src/resources/tables.ts
|
|
3376
|
-
async function tablesResourceHandler(uri, variables, _extra) {
|
|
3377
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3378
|
-
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3379
|
-
try {
|
|
3380
|
-
if (schemaName) {
|
|
3381
|
-
const availableSchemas = await connector.getSchemas();
|
|
3382
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3383
|
-
return createResourceErrorResponse(
|
|
3384
|
-
uri.href,
|
|
3385
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3386
|
-
"SCHEMA_NOT_FOUND"
|
|
3387
|
-
);
|
|
3388
|
-
}
|
|
3389
|
-
}
|
|
3390
|
-
const tableNames = await connector.getTables(schemaName);
|
|
3391
|
-
const responseData = {
|
|
3392
|
-
tables: tableNames,
|
|
3393
|
-
count: tableNames.length,
|
|
3394
|
-
schema: schemaName
|
|
3395
|
-
};
|
|
3396
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3397
|
-
} catch (error) {
|
|
3398
|
-
return createResourceErrorResponse(
|
|
3399
|
-
uri.href,
|
|
3400
|
-
`Error retrieving tables: ${error.message}`,
|
|
3401
|
-
"TABLES_RETRIEVAL_ERROR"
|
|
3402
|
-
);
|
|
3403
|
-
}
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
// src/resources/schema.ts
|
|
3407
|
-
async function tableStructureResourceHandler(uri, variables, _extra) {
|
|
3408
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3409
|
-
const tableName = Array.isArray(variables.tableName) ? variables.tableName[0] : variables.tableName;
|
|
3410
|
-
const schemaName = variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3411
|
-
try {
|
|
3412
|
-
if (schemaName) {
|
|
3413
|
-
const availableSchemas = await connector.getSchemas();
|
|
3414
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3415
|
-
return createResourceErrorResponse(
|
|
3416
|
-
uri.href,
|
|
3417
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3418
|
-
"SCHEMA_NOT_FOUND"
|
|
3419
|
-
);
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
const tableExists = await connector.tableExists(tableName, schemaName);
|
|
3423
|
-
if (!tableExists) {
|
|
3424
|
-
const schemaInfo = schemaName ? ` in schema '${schemaName}'` : "";
|
|
3425
|
-
return createResourceErrorResponse(
|
|
3426
|
-
uri.href,
|
|
3427
|
-
`Table '${tableName}'${schemaInfo} does not exist or cannot be accessed`,
|
|
3428
|
-
"TABLE_NOT_FOUND"
|
|
3429
|
-
);
|
|
3430
|
-
}
|
|
3431
|
-
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
3432
|
-
const formattedColumns = columns.map((col) => ({
|
|
3433
|
-
name: col.column_name,
|
|
3434
|
-
type: col.data_type,
|
|
3435
|
-
nullable: col.is_nullable === "YES",
|
|
3436
|
-
default: col.column_default
|
|
3437
|
-
}));
|
|
3438
|
-
const responseData = {
|
|
3439
|
-
table: tableName,
|
|
3440
|
-
schema: schemaName,
|
|
3441
|
-
columns: formattedColumns,
|
|
3442
|
-
count: formattedColumns.length
|
|
3443
|
-
};
|
|
3444
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3445
|
-
} catch (error) {
|
|
3446
|
-
return createResourceErrorResponse(
|
|
3447
|
-
uri.href,
|
|
3448
|
-
`Error retrieving schema: ${error.message}`,
|
|
3449
|
-
"SCHEMA_RETRIEVAL_ERROR"
|
|
3450
|
-
);
|
|
3451
|
-
}
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
|
-
// src/resources/schemas.ts
|
|
3455
|
-
async function schemasResourceHandler(uri, _extra) {
|
|
3456
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3457
|
-
try {
|
|
3458
|
-
const schemas = await connector.getSchemas();
|
|
3459
|
-
const responseData = {
|
|
3460
|
-
schemas,
|
|
3461
|
-
count: schemas.length
|
|
3462
|
-
};
|
|
3463
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3464
|
-
} catch (error) {
|
|
3465
|
-
return createResourceErrorResponse(
|
|
3466
|
-
uri.href,
|
|
3467
|
-
`Error retrieving database schemas: ${error.message}`,
|
|
3468
|
-
"SCHEMAS_RETRIEVAL_ERROR"
|
|
3469
|
-
);
|
|
3470
|
-
}
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
// src/resources/indexes.ts
|
|
3474
|
-
async function indexesResourceHandler(uri, variables, _extra) {
|
|
3475
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3476
|
-
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3477
|
-
const tableName = variables && variables.tableName ? Array.isArray(variables.tableName) ? variables.tableName[0] : variables.tableName : void 0;
|
|
3478
|
-
if (!tableName) {
|
|
3479
|
-
return createResourceErrorResponse(uri.href, "Table name is required", "MISSING_TABLE_NAME");
|
|
3480
|
-
}
|
|
3481
|
-
try {
|
|
3482
|
-
if (schemaName) {
|
|
3483
|
-
const availableSchemas = await connector.getSchemas();
|
|
3484
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3485
|
-
return createResourceErrorResponse(
|
|
3486
|
-
uri.href,
|
|
3487
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3488
|
-
"SCHEMA_NOT_FOUND"
|
|
3489
|
-
);
|
|
3490
|
-
}
|
|
3491
|
-
}
|
|
3492
|
-
const tableExists = await connector.tableExists(tableName, schemaName);
|
|
3493
|
-
if (!tableExists) {
|
|
3494
|
-
return createResourceErrorResponse(
|
|
3495
|
-
uri.href,
|
|
3496
|
-
`Table '${tableName}' does not exist in schema '${schemaName || "default"}'`,
|
|
3497
|
-
"TABLE_NOT_FOUND"
|
|
3498
|
-
);
|
|
3499
|
-
}
|
|
3500
|
-
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
3501
|
-
const responseData = {
|
|
3502
|
-
table: tableName,
|
|
3503
|
-
schema: schemaName,
|
|
3504
|
-
indexes,
|
|
3505
|
-
count: indexes.length
|
|
3506
|
-
};
|
|
3507
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3508
|
-
} catch (error) {
|
|
3509
|
-
return createResourceErrorResponse(
|
|
3510
|
-
uri.href,
|
|
3511
|
-
`Error retrieving indexes: ${error.message}`,
|
|
3512
|
-
"INDEXES_RETRIEVAL_ERROR"
|
|
3513
|
-
);
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
|
-
// src/resources/procedures.ts
|
|
3518
|
-
async function proceduresResourceHandler(uri, variables, _extra) {
|
|
3519
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3520
|
-
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3521
|
-
try {
|
|
3522
|
-
if (schemaName) {
|
|
3523
|
-
const availableSchemas = await connector.getSchemas();
|
|
3524
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3525
|
-
return createResourceErrorResponse(
|
|
3526
|
-
uri.href,
|
|
3527
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3528
|
-
"SCHEMA_NOT_FOUND"
|
|
3529
|
-
);
|
|
3530
|
-
}
|
|
3531
|
-
}
|
|
3532
|
-
const procedureNames = await connector.getStoredProcedures(schemaName);
|
|
3533
|
-
const responseData = {
|
|
3534
|
-
procedures: procedureNames,
|
|
3535
|
-
count: procedureNames.length,
|
|
3536
|
-
schema: schemaName
|
|
3537
|
-
};
|
|
3538
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3539
|
-
} catch (error) {
|
|
3540
|
-
return createResourceErrorResponse(
|
|
3541
|
-
uri.href,
|
|
3542
|
-
`Error retrieving stored procedures: ${error.message}`,
|
|
3543
|
-
"PROCEDURES_RETRIEVAL_ERROR"
|
|
3544
|
-
);
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3547
|
-
async function procedureDetailResourceHandler(uri, variables, _extra) {
|
|
3548
|
-
const connector = ConnectorManager.getCurrentConnector();
|
|
3549
|
-
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
3550
|
-
const procedureName = variables && variables.procedureName ? Array.isArray(variables.procedureName) ? variables.procedureName[0] : variables.procedureName : void 0;
|
|
3551
|
-
if (!procedureName) {
|
|
3552
|
-
return createResourceErrorResponse(uri.href, "Procedure name is required", "MISSING_PARAMETER");
|
|
3553
|
-
}
|
|
3554
|
-
try {
|
|
3555
|
-
if (schemaName) {
|
|
3556
|
-
const availableSchemas = await connector.getSchemas();
|
|
3557
|
-
if (!availableSchemas.includes(schemaName)) {
|
|
3558
|
-
return createResourceErrorResponse(
|
|
3559
|
-
uri.href,
|
|
3560
|
-
`Schema '${schemaName}' does not exist or cannot be accessed`,
|
|
3561
|
-
"SCHEMA_NOT_FOUND"
|
|
3562
|
-
);
|
|
3563
|
-
}
|
|
3564
|
-
}
|
|
3565
|
-
const procedureDetails = await connector.getStoredProcedureDetail(procedureName, schemaName);
|
|
3566
|
-
const responseData = {
|
|
3567
|
-
procedureName: procedureDetails.procedure_name,
|
|
3568
|
-
procedureType: procedureDetails.procedure_type,
|
|
3569
|
-
language: procedureDetails.language,
|
|
3570
|
-
parameters: procedureDetails.parameter_list,
|
|
3571
|
-
returnType: procedureDetails.return_type,
|
|
3572
|
-
definition: procedureDetails.definition,
|
|
3573
|
-
schema: schemaName
|
|
3574
|
-
};
|
|
3575
|
-
return createResourceSuccessResponse(uri.href, responseData);
|
|
3576
|
-
} catch (error) {
|
|
3577
|
-
return createResourceErrorResponse(
|
|
3578
|
-
uri.href,
|
|
3579
|
-
`Error retrieving procedure details: ${error.message}`,
|
|
3580
|
-
"PROCEDURE_DETAILS_ERROR"
|
|
3581
|
-
);
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
|
|
3585
|
-
// src/resources/index.ts
|
|
3586
|
-
function registerResources(server) {
|
|
3587
|
-
server.resource(
|
|
3588
|
-
"schemas",
|
|
3589
|
-
"db://schemas",
|
|
3590
|
-
{
|
|
3591
|
-
description: "List all schemas/databases available in the connected database",
|
|
3592
|
-
mimeType: "application/json"
|
|
3593
|
-
},
|
|
3594
|
-
schemasResourceHandler
|
|
3595
|
-
);
|
|
3596
|
-
server.resource(
|
|
3597
|
-
"tables_in_schema",
|
|
3598
|
-
new ResourceTemplate("db://schemas/{schemaName}/tables", { list: void 0 }),
|
|
3599
|
-
{
|
|
3600
|
-
description: "List all tables within a specific schema",
|
|
3601
|
-
mimeType: "application/json"
|
|
3602
|
-
},
|
|
3603
|
-
tablesResourceHandler
|
|
3604
|
-
);
|
|
3605
|
-
server.resource(
|
|
3606
|
-
"table_structure_in_schema",
|
|
3607
|
-
new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}", { list: void 0 }),
|
|
3608
|
-
{
|
|
3609
|
-
description: "Get detailed structure information for a specific table, including columns, data types, and constraints",
|
|
3610
|
-
mimeType: "application/json"
|
|
3611
|
-
},
|
|
3612
|
-
tableStructureResourceHandler
|
|
3613
|
-
);
|
|
3614
|
-
server.resource(
|
|
3615
|
-
"indexes_in_table",
|
|
3616
|
-
new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}/indexes", {
|
|
3617
|
-
list: void 0
|
|
3618
|
-
}),
|
|
3619
|
-
{
|
|
3620
|
-
description: "List all indexes defined on a specific table",
|
|
3621
|
-
mimeType: "application/json"
|
|
3622
|
-
},
|
|
3623
|
-
indexesResourceHandler
|
|
3624
|
-
);
|
|
3625
|
-
server.resource(
|
|
3626
|
-
"procedures_in_schema",
|
|
3627
|
-
new ResourceTemplate("db://schemas/{schemaName}/procedures", { list: void 0 }),
|
|
3628
|
-
{
|
|
3629
|
-
description: "List all stored procedures/functions in a schema (not supported by SQLite)",
|
|
3630
|
-
mimeType: "application/json"
|
|
3631
|
-
},
|
|
3632
|
-
proceduresResourceHandler
|
|
3633
|
-
);
|
|
3634
|
-
server.resource(
|
|
3635
|
-
"procedure_detail_in_schema",
|
|
3636
|
-
new ResourceTemplate("db://schemas/{schemaName}/procedures/{procedureName}", {
|
|
3637
|
-
list: void 0
|
|
3638
|
-
}),
|
|
3639
|
-
{
|
|
3640
|
-
description: "Get detailed information about a specific stored procedure, including parameters and definition (not supported by SQLite)",
|
|
3641
|
-
mimeType: "application/json"
|
|
3642
|
-
},
|
|
3643
|
-
procedureDetailResourceHandler
|
|
3644
|
-
);
|
|
3645
|
-
}
|
|
3646
|
-
|
|
3647
|
-
// src/tools/execute-sql.ts
|
|
3648
|
-
import { z } from "zod";
|
|
3649
|
-
|
|
3650
|
-
// src/utils/allowed-keywords.ts
|
|
3651
|
-
var allowedKeywords = {
|
|
3652
|
-
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
3653
|
-
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
3654
|
-
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
3655
|
-
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
3656
|
-
sqlserver: ["select", "with", "explain", "showplan"]
|
|
3657
|
-
};
|
|
3658
|
-
|
|
3659
|
-
// src/requests/store.ts
|
|
3660
|
-
var RequestStore = class {
|
|
3661
|
-
constructor() {
|
|
3662
|
-
this.store = /* @__PURE__ */ new Map();
|
|
3663
|
-
this.maxPerSource = 100;
|
|
3664
|
-
}
|
|
3665
|
-
/**
|
|
3666
|
-
* Add a request to the store
|
|
3667
|
-
* Evicts oldest entry if at capacity
|
|
3668
|
-
*/
|
|
3669
|
-
add(request) {
|
|
3670
|
-
const requests = this.store.get(request.sourceId) ?? [];
|
|
3671
|
-
requests.push(request);
|
|
3672
|
-
if (requests.length > this.maxPerSource) {
|
|
3673
|
-
requests.shift();
|
|
3674
|
-
}
|
|
3675
|
-
this.store.set(request.sourceId, requests);
|
|
3676
|
-
}
|
|
3677
|
-
/**
|
|
3678
|
-
* Get requests, optionally filtered by source
|
|
3679
|
-
* Returns newest first
|
|
3680
|
-
*/
|
|
3681
|
-
getAll(sourceId) {
|
|
3682
|
-
let requests;
|
|
3683
|
-
if (sourceId) {
|
|
3684
|
-
requests = [...this.store.get(sourceId) ?? []];
|
|
3685
|
-
} else {
|
|
3686
|
-
requests = Array.from(this.store.values()).flat();
|
|
3687
|
-
}
|
|
3688
|
-
return requests.sort(
|
|
3689
|
-
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
3690
|
-
);
|
|
3691
|
-
}
|
|
3692
|
-
/**
|
|
3693
|
-
* Get total count of requests across all sources
|
|
3694
|
-
*/
|
|
3695
|
-
getTotal() {
|
|
3696
|
-
return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
|
|
3697
|
-
}
|
|
3698
|
-
/**
|
|
3699
|
-
* Clear all requests (useful for testing)
|
|
3700
|
-
*/
|
|
3701
|
-
clear() {
|
|
3702
|
-
this.store.clear();
|
|
3703
|
-
}
|
|
3704
|
-
};
|
|
3705
|
-
|
|
3706
|
-
// src/requests/index.ts
|
|
3707
|
-
var requestStore = new RequestStore();
|
|
3708
|
-
|
|
3709
|
-
// src/tools/execute-sql.ts
|
|
2221
|
+
// src/utils/client-identifier.ts
|
|
3710
2222
|
function getClientIdentifier(extra) {
|
|
3711
2223
|
const userAgent = extra?.requestInfo?.headers?.["user-agent"];
|
|
3712
2224
|
if (userAgent) {
|
|
@@ -3714,28 +2226,13 @@ function getClientIdentifier(extra) {
|
|
|
3714
2226
|
}
|
|
3715
2227
|
return "stdio";
|
|
3716
2228
|
}
|
|
2229
|
+
|
|
2230
|
+
// src/tools/execute-sql.ts
|
|
3717
2231
|
var executeSqlSchema = {
|
|
3718
|
-
sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
|
|
3719
|
-
};
|
|
3720
|
-
function splitSQLStatements(sql2) {
|
|
3721
|
-
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
3722
|
-
}
|
|
3723
|
-
function stripSQLComments(sql2) {
|
|
3724
|
-
let cleaned = sql2.split("\n").map((line) => {
|
|
3725
|
-
const commentIndex = line.indexOf("--");
|
|
3726
|
-
return commentIndex >= 0 ? line.substring(0, commentIndex) : line;
|
|
3727
|
-
}).join("\n");
|
|
3728
|
-
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
3729
|
-
return cleaned.trim();
|
|
3730
|
-
}
|
|
3731
|
-
function isReadOnlySQL(sql2, connectorType) {
|
|
3732
|
-
const cleanedSQL = stripSQLComments(sql2).toLowerCase();
|
|
3733
|
-
if (!cleanedSQL) {
|
|
3734
|
-
return true;
|
|
3735
|
-
}
|
|
3736
|
-
const firstWord = cleanedSQL.split(/\s+/)[0];
|
|
3737
|
-
const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
|
|
3738
|
-
return keywordList.includes(firstWord);
|
|
2232
|
+
sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
|
|
2233
|
+
};
|
|
2234
|
+
function splitSQLStatements(sql2) {
|
|
2235
|
+
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
3739
2236
|
}
|
|
3740
2237
|
function areAllStatementsReadOnly(sql2, connectorType) {
|
|
3741
2238
|
const statements = splitSQLStatements(sql2);
|
|
@@ -3751,13 +2248,19 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
3751
2248
|
let result;
|
|
3752
2249
|
try {
|
|
3753
2250
|
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
3754
|
-
const
|
|
3755
|
-
const
|
|
2251
|
+
const actualSourceId = connector.getId();
|
|
2252
|
+
const registry = getToolRegistry();
|
|
2253
|
+
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
|
|
2254
|
+
const isReadonly = toolConfig?.readonly === true;
|
|
3756
2255
|
if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
|
|
3757
|
-
errorMessage = `Read-only mode is enabled for source '${
|
|
2256
|
+
errorMessage = `Read-only mode is enabled for source '${actualSourceId}'. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
3758
2257
|
success = false;
|
|
3759
2258
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
3760
2259
|
}
|
|
2260
|
+
const executeOptions = {
|
|
2261
|
+
readonly: isReadonly,
|
|
2262
|
+
max_rows: toolConfig?.max_rows
|
|
2263
|
+
};
|
|
3761
2264
|
result = await connector.executeSQL(sql2, executeOptions);
|
|
3762
2265
|
const responseData = {
|
|
3763
2266
|
rows: result.rows,
|
|
@@ -3790,7 +2293,8 @@ import { z as z2 } from "zod";
|
|
|
3790
2293
|
var searchDatabaseObjectsSchema = {
|
|
3791
2294
|
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Type of database object to search for"),
|
|
3792
2295
|
pattern: z2.string().optional().default("%").describe("Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."),
|
|
3793
|
-
schema: z2.string().optional().describe("Filter results to a specific schema/database"),
|
|
2296
|
+
schema: z2.string().optional().describe("Filter results to a specific schema/database (exact match)"),
|
|
2297
|
+
table: z2.string().optional().describe("Filter to a specific table (exact match). Requires schema parameter. Only applies to columns and indexes."),
|
|
3794
2298
|
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Level of detail to return: names (minimal), summary (with metadata), full (complete structure)"),
|
|
3795
2299
|
limit: z2.number().int().positive().max(1e3).default(100).describe("Maximum number of results to return (default: 100, max: 1000)")
|
|
3796
2300
|
};
|
|
@@ -3913,7 +2417,7 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
3913
2417
|
}
|
|
3914
2418
|
return results;
|
|
3915
2419
|
}
|
|
3916
|
-
async function searchColumns(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2420
|
+
async function searchColumns(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
3917
2421
|
const regex = likePatternToRegex(pattern);
|
|
3918
2422
|
const results = [];
|
|
3919
2423
|
let schemasToSearch;
|
|
@@ -3925,8 +2429,13 @@ async function searchColumns(connector, pattern, schemaFilter, detailLevel, limi
|
|
|
3925
2429
|
for (const schemaName of schemasToSearch) {
|
|
3926
2430
|
if (results.length >= limit) break;
|
|
3927
2431
|
try {
|
|
3928
|
-
|
|
3929
|
-
|
|
2432
|
+
let tablesToSearch;
|
|
2433
|
+
if (tableFilter) {
|
|
2434
|
+
tablesToSearch = [tableFilter];
|
|
2435
|
+
} else {
|
|
2436
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
2437
|
+
}
|
|
2438
|
+
for (const tableName of tablesToSearch) {
|
|
3930
2439
|
if (results.length >= limit) break;
|
|
3931
2440
|
try {
|
|
3932
2441
|
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
@@ -4008,7 +2517,7 @@ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, l
|
|
|
4008
2517
|
}
|
|
4009
2518
|
return results;
|
|
4010
2519
|
}
|
|
4011
|
-
async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
2520
|
+
async function searchIndexes(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
|
|
4012
2521
|
const regex = likePatternToRegex(pattern);
|
|
4013
2522
|
const results = [];
|
|
4014
2523
|
let schemasToSearch;
|
|
@@ -4020,8 +2529,13 @@ async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limi
|
|
|
4020
2529
|
for (const schemaName of schemasToSearch) {
|
|
4021
2530
|
if (results.length >= limit) break;
|
|
4022
2531
|
try {
|
|
4023
|
-
|
|
4024
|
-
|
|
2532
|
+
let tablesToSearch;
|
|
2533
|
+
if (tableFilter) {
|
|
2534
|
+
tablesToSearch = [tableFilter];
|
|
2535
|
+
} else {
|
|
2536
|
+
tablesToSearch = await connector.getTables(schemaName);
|
|
2537
|
+
}
|
|
2538
|
+
for (const tableName of tablesToSearch) {
|
|
4025
2539
|
if (results.length >= limit) break;
|
|
4026
2540
|
try {
|
|
4027
2541
|
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
@@ -4061,11 +2575,26 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
4061
2575
|
object_type,
|
|
4062
2576
|
pattern = "%",
|
|
4063
2577
|
schema,
|
|
2578
|
+
table,
|
|
4064
2579
|
detail_level = "names",
|
|
4065
2580
|
limit = 100
|
|
4066
2581
|
} = args;
|
|
4067
2582
|
try {
|
|
4068
2583
|
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
2584
|
+
if (table) {
|
|
2585
|
+
if (!schema) {
|
|
2586
|
+
return createToolErrorResponse(
|
|
2587
|
+
"The 'table' parameter requires 'schema' to be specified",
|
|
2588
|
+
"SCHEMA_REQUIRED"
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
if (!["column", "index"].includes(object_type)) {
|
|
2592
|
+
return createToolErrorResponse(
|
|
2593
|
+
`The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`,
|
|
2594
|
+
"INVALID_TABLE_FILTER"
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
4069
2598
|
if (schema) {
|
|
4070
2599
|
const schemas = await connector.getSchemas();
|
|
4071
2600
|
if (!schemas.includes(schema)) {
|
|
@@ -4084,13 +2613,13 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
4084
2613
|
results = await searchTables(connector, pattern, schema, detail_level, limit);
|
|
4085
2614
|
break;
|
|
4086
2615
|
case "column":
|
|
4087
|
-
results = await searchColumns(connector, pattern, schema, detail_level, limit);
|
|
2616
|
+
results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
|
|
4088
2617
|
break;
|
|
4089
2618
|
case "procedure":
|
|
4090
2619
|
results = await searchProcedures(connector, pattern, schema, detail_level, limit);
|
|
4091
2620
|
break;
|
|
4092
2621
|
case "index":
|
|
4093
|
-
results = await searchIndexes(connector, pattern, schema, detail_level, limit);
|
|
2622
|
+
results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
|
|
4094
2623
|
break;
|
|
4095
2624
|
default:
|
|
4096
2625
|
return createToolErrorResponse(
|
|
@@ -4102,6 +2631,7 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
4102
2631
|
object_type,
|
|
4103
2632
|
pattern,
|
|
4104
2633
|
schema,
|
|
2634
|
+
table,
|
|
4105
2635
|
detail_level,
|
|
4106
2636
|
count: results.length,
|
|
4107
2637
|
results,
|
|
@@ -4151,11 +2681,11 @@ function zodToParameters(schema) {
|
|
|
4151
2681
|
}
|
|
4152
2682
|
return parameters;
|
|
4153
2683
|
}
|
|
4154
|
-
function
|
|
2684
|
+
function getExecuteSqlMetadata(sourceId) {
|
|
4155
2685
|
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
4156
2686
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
4157
2687
|
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
|
|
4158
|
-
const dbType = sourceConfig
|
|
2688
|
+
const dbType = sourceConfig.type;
|
|
4159
2689
|
const toolName = sourceId === "default" ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
4160
2690
|
const isDefault = sourceIds[0] === sourceId;
|
|
4161
2691
|
const title = isDefault ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
@@ -4171,9 +2701,8 @@ function getToolMetadataForSource(sourceId) {
|
|
|
4171
2701
|
// In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
|
|
4172
2702
|
// In write mode, queries are definitely not idempotent
|
|
4173
2703
|
idempotentHint: false,
|
|
4174
|
-
//
|
|
4175
|
-
|
|
4176
|
-
openWorldHint: isReadonly
|
|
2704
|
+
// Database operations are always against internal/closed systems, not open-world
|
|
2705
|
+
openWorldHint: false
|
|
4177
2706
|
};
|
|
4178
2707
|
return {
|
|
4179
2708
|
name: toolName,
|
|
@@ -4182,473 +2711,283 @@ function getToolMetadataForSource(sourceId) {
|
|
|
4182
2711
|
annotations
|
|
4183
2712
|
};
|
|
4184
2713
|
}
|
|
4185
|
-
function
|
|
4186
|
-
const
|
|
4187
|
-
const
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
];
|
|
2714
|
+
function getSearchObjectsMetadata(sourceId, dbType, isDefault) {
|
|
2715
|
+
const toolName = sourceId === "default" ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
2716
|
+
const title = isDefault ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
2717
|
+
const description = `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}. Supports SQL LIKE patterns (default: '%' for all), filtering, and token-efficient progressive disclosure.`;
|
|
2718
|
+
return {
|
|
2719
|
+
name: toolName,
|
|
2720
|
+
description,
|
|
2721
|
+
title
|
|
2722
|
+
};
|
|
4195
2723
|
}
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
4200
|
-
if (sourceIds.length === 0) {
|
|
4201
|
-
throw new Error("No database sources configured");
|
|
2724
|
+
function customParamsToToolParams(params) {
|
|
2725
|
+
if (!params || params.length === 0) {
|
|
2726
|
+
return [];
|
|
4202
2727
|
}
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
2728
|
+
return params.map((param) => ({
|
|
2729
|
+
name: param.name,
|
|
2730
|
+
type: param.type,
|
|
2731
|
+
required: param.required !== false && param.default === void 0,
|
|
2732
|
+
description: param.description
|
|
2733
|
+
}));
|
|
2734
|
+
}
|
|
2735
|
+
function buildExecuteSqlTool(sourceId) {
|
|
2736
|
+
const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
|
|
2737
|
+
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
|
|
2738
|
+
return {
|
|
2739
|
+
name: executeSqlMetadata.name,
|
|
2740
|
+
description: executeSqlMetadata.description,
|
|
2741
|
+
parameters: executeSqlParameters
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
function buildSearchObjectsTool(sourceId) {
|
|
2745
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2746
|
+
const dbType = sourceConfig.type;
|
|
2747
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2748
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
2749
|
+
const searchMetadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
|
|
2750
|
+
return {
|
|
2751
|
+
name: searchMetadata.name,
|
|
2752
|
+
description: searchMetadata.description,
|
|
2753
|
+
parameters: [
|
|
4210
2754
|
{
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
2755
|
+
name: "object_type",
|
|
2756
|
+
type: "string",
|
|
2757
|
+
required: true,
|
|
2758
|
+
description: "Type of database object to search for (schema, table, column, procedure, index)"
|
|
4214
2759
|
},
|
|
4215
|
-
createExecuteSqlToolHandler(sourceId)
|
|
4216
|
-
);
|
|
4217
|
-
const searchToolName = sourceId === "default" ? "search_objects" : `search_objects_${sourceId}`;
|
|
4218
|
-
const searchToolTitle = isDefault ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
4219
|
-
const searchToolDescription = `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}. Supports SQL LIKE patterns (default: '%' for all), filtering, and token-efficient progressive disclosure.`;
|
|
4220
|
-
server.registerTool(
|
|
4221
|
-
searchToolName,
|
|
4222
2760
|
{
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
readOnlyHint: true,
|
|
4228
|
-
destructiveHint: false,
|
|
4229
|
-
idempotentHint: true,
|
|
4230
|
-
// Operation is read-only and idempotent
|
|
4231
|
-
openWorldHint: true
|
|
4232
|
-
}
|
|
2761
|
+
name: "pattern",
|
|
2762
|
+
type: "string",
|
|
2763
|
+
required: false,
|
|
2764
|
+
description: "Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."
|
|
4233
2765
|
},
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
2766
|
+
{
|
|
2767
|
+
name: "schema",
|
|
2768
|
+
type: "string",
|
|
2769
|
+
required: false,
|
|
2770
|
+
description: "Filter results to a specific schema/database"
|
|
2771
|
+
},
|
|
2772
|
+
{
|
|
2773
|
+
name: "detail_level",
|
|
2774
|
+
type: "string",
|
|
2775
|
+
required: false,
|
|
2776
|
+
description: "Level of detail to return: names (minimal), summary (with metadata), full (complete structure). Defaults to 'names'."
|
|
2777
|
+
},
|
|
2778
|
+
{
|
|
2779
|
+
name: "limit",
|
|
2780
|
+
type: "integer",
|
|
2781
|
+
required: false,
|
|
2782
|
+
description: "Maximum number of results to return (default: 100, max: 1000)"
|
|
2783
|
+
}
|
|
2784
|
+
]
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
function buildCustomTool(toolConfig) {
|
|
2788
|
+
return {
|
|
2789
|
+
name: toolConfig.name,
|
|
2790
|
+
description: toolConfig.description,
|
|
2791
|
+
parameters: customParamsToToolParams(toolConfig.parameters)
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
function getToolsForSource(sourceId) {
|
|
2795
|
+
const registry = getToolRegistry();
|
|
2796
|
+
const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
|
|
2797
|
+
return enabledToolConfigs.map((toolConfig) => {
|
|
2798
|
+
if (toolConfig.name === "execute_sql") {
|
|
2799
|
+
return buildExecuteSqlTool(sourceId);
|
|
2800
|
+
} else if (toolConfig.name === "search_objects") {
|
|
2801
|
+
return buildSearchObjectsTool(sourceId);
|
|
2802
|
+
} else {
|
|
2803
|
+
return buildCustomTool(toolConfig);
|
|
2804
|
+
}
|
|
2805
|
+
});
|
|
4237
2806
|
}
|
|
4238
2807
|
|
|
4239
|
-
// src/
|
|
2808
|
+
// src/tools/custom-tool-handler.ts
|
|
4240
2809
|
import { z as z4 } from "zod";
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
}
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
sqlDialect = "postgres";
|
|
2810
|
+
function buildZodSchemaFromParameters(parameters) {
|
|
2811
|
+
if (!parameters || parameters.length === 0) {
|
|
2812
|
+
return {};
|
|
2813
|
+
}
|
|
2814
|
+
const schemaShape = {};
|
|
2815
|
+
for (const param of parameters) {
|
|
2816
|
+
let fieldSchema;
|
|
2817
|
+
switch (param.type) {
|
|
2818
|
+
case "string":
|
|
2819
|
+
fieldSchema = z4.string().describe(param.description);
|
|
2820
|
+
break;
|
|
2821
|
+
case "integer":
|
|
2822
|
+
fieldSchema = z4.number().int().describe(param.description);
|
|
4255
2823
|
break;
|
|
4256
|
-
case "
|
|
4257
|
-
|
|
2824
|
+
case "float":
|
|
2825
|
+
fieldSchema = z4.number().describe(param.description);
|
|
4258
2826
|
break;
|
|
4259
|
-
case "
|
|
4260
|
-
|
|
2827
|
+
case "boolean":
|
|
2828
|
+
fieldSchema = z4.boolean().describe(param.description);
|
|
4261
2829
|
break;
|
|
4262
|
-
case "
|
|
4263
|
-
|
|
2830
|
+
case "array":
|
|
2831
|
+
fieldSchema = z4.array(z4.unknown()).describe(param.description);
|
|
4264
2832
|
break;
|
|
4265
2833
|
default:
|
|
4266
|
-
|
|
4267
|
-
}
|
|
4268
|
-
if (schema) {
|
|
4269
|
-
const availableSchemas = await connector.getSchemas();
|
|
4270
|
-
if (!availableSchemas.includes(schema)) {
|
|
4271
|
-
return formatPromptErrorResponse(
|
|
4272
|
-
`Schema '${schema}' does not exist or cannot be accessed. Available schemas: ${availableSchemas.join(", ")}`,
|
|
4273
|
-
"SCHEMA_NOT_FOUND"
|
|
4274
|
-
);
|
|
4275
|
-
}
|
|
2834
|
+
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
4276
2835
|
}
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
}
|
|
4286
|
-
const tableSchemas = await Promise.all(
|
|
4287
|
-
tables.map(async (table) => {
|
|
4288
|
-
try {
|
|
4289
|
-
const columns = await connector.getTableSchema(table, schema);
|
|
4290
|
-
return {
|
|
4291
|
-
table,
|
|
4292
|
-
columns: columns.map((col) => ({
|
|
4293
|
-
name: col.column_name,
|
|
4294
|
-
type: col.data_type
|
|
4295
|
-
}))
|
|
4296
|
-
};
|
|
4297
|
-
} catch (error) {
|
|
4298
|
-
return null;
|
|
2836
|
+
if (param.allowed_values && param.allowed_values.length > 0) {
|
|
2837
|
+
if (param.type === "string") {
|
|
2838
|
+
fieldSchema = z4.enum(param.allowed_values).describe(param.description);
|
|
2839
|
+
} else {
|
|
2840
|
+
fieldSchema = fieldSchema.refine(
|
|
2841
|
+
(val) => param.allowed_values.includes(val),
|
|
2842
|
+
{
|
|
2843
|
+
message: `Value must be one of: ${param.allowed_values.join(", ")}`
|
|
4299
2844
|
}
|
|
4300
|
-
})
|
|
4301
|
-
);
|
|
4302
|
-
const accessibleSchemas = tableSchemas.filter((schema2) => schema2 !== null);
|
|
4303
|
-
if (accessibleSchemas.length === 0) {
|
|
4304
|
-
return formatPromptErrorResponse(
|
|
4305
|
-
`No accessible tables found. You may not have sufficient permissions to access table schemas.`,
|
|
4306
|
-
"NO_ACCESSIBLE_TABLES"
|
|
4307
2845
|
);
|
|
4308
2846
|
}
|
|
4309
|
-
const schemaContext = accessibleSchemas.length > 0 ? `Available tables and their columns:
|
|
4310
|
-
${accessibleSchemas.map(
|
|
4311
|
-
(schema2) => `- ${schema2.table}: ${schema2.columns.map((col) => `${col.name} (${col.type})`).join(", ")}`
|
|
4312
|
-
).join("\n")}` : "No schema information available.";
|
|
4313
|
-
const dialectExamples = {
|
|
4314
|
-
postgres: [
|
|
4315
|
-
"SELECT * FROM users WHERE created_at > NOW() - INTERVAL '1 day'",
|
|
4316
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
4317
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
4318
|
-
],
|
|
4319
|
-
sqlite: [
|
|
4320
|
-
"SELECT * FROM users WHERE created_at > datetime('now', '-1 day')",
|
|
4321
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
4322
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
4323
|
-
],
|
|
4324
|
-
mysql: [
|
|
4325
|
-
"SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY",
|
|
4326
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
4327
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
4328
|
-
],
|
|
4329
|
-
mssql: [
|
|
4330
|
-
"SELECT * FROM users WHERE created_at > DATEADD(day, -1, GETDATE())",
|
|
4331
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
4332
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
4333
|
-
],
|
|
4334
|
-
ansi: [
|
|
4335
|
-
"SELECT * FROM users WHERE created_at > CURRENT_TIMESTAMP - INTERVAL '1' DAY",
|
|
4336
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
4337
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
4338
|
-
]
|
|
4339
|
-
};
|
|
4340
|
-
const schemaInfo = schema ? `in schema '${schema}'` : "across all schemas";
|
|
4341
|
-
const prompt = `
|
|
4342
|
-
Generate a ${sqlDialect} SQL query based on this description: "${description}"
|
|
4343
|
-
|
|
4344
|
-
${schemaContext}
|
|
4345
|
-
Working ${schemaInfo}
|
|
4346
|
-
|
|
4347
|
-
The query should:
|
|
4348
|
-
1. Be written for ${sqlDialect} dialect
|
|
4349
|
-
2. Use only the available tables and columns
|
|
4350
|
-
3. Prioritize readability
|
|
4351
|
-
4. Include appropriate comments
|
|
4352
|
-
5. Be compatible with ${sqlDialect} syntax
|
|
4353
|
-
`;
|
|
4354
|
-
let generatedSQL;
|
|
4355
|
-
if (description.toLowerCase().includes("count")) {
|
|
4356
|
-
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
4357
|
-
` : "";
|
|
4358
|
-
generatedSQL = `${schemaPrefix}-- Count query generated from: "${description}"
|
|
4359
|
-
SELECT COUNT(*) AS count
|
|
4360
|
-
FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name"};`;
|
|
4361
|
-
} else if (description.toLowerCase().includes("average") || description.toLowerCase().includes("avg")) {
|
|
4362
|
-
const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name";
|
|
4363
|
-
const numericColumn = accessibleSchemas.length > 0 ? accessibleSchemas[0].columns.find(
|
|
4364
|
-
(col) => ["int", "numeric", "decimal", "float", "real", "double"].some(
|
|
4365
|
-
(t) => col.type.includes(t)
|
|
4366
|
-
)
|
|
4367
|
-
)?.name || "numeric_column" : "numeric_column";
|
|
4368
|
-
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
4369
|
-
` : "";
|
|
4370
|
-
generatedSQL = `${schemaPrefix}-- Average query generated from: "${description}"
|
|
4371
|
-
SELECT AVG(${numericColumn}) AS average
|
|
4372
|
-
FROM ${table};`;
|
|
4373
|
-
} else if (description.toLowerCase().includes("join")) {
|
|
4374
|
-
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
4375
|
-
` : "";
|
|
4376
|
-
generatedSQL = `${schemaPrefix}-- Join query generated from: "${description}"
|
|
4377
|
-
SELECT t1.*, t2.*
|
|
4378
|
-
FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : "table1"} t1
|
|
4379
|
-
JOIN ${accessibleSchemas.length > 1 ? accessibleSchemas[1]?.table : "table2"} t2
|
|
4380
|
-
ON t1.id = t2.${accessibleSchemas.length > 0 ? accessibleSchemas[0]?.table : "table1"}_id;`;
|
|
4381
|
-
} else {
|
|
4382
|
-
const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name";
|
|
4383
|
-
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
4384
|
-
` : "";
|
|
4385
|
-
generatedSQL = `${schemaPrefix}-- Query generated from: "${description}"
|
|
4386
|
-
SELECT *
|
|
4387
|
-
FROM ${table}
|
|
4388
|
-
LIMIT 10;`;
|
|
4389
|
-
}
|
|
4390
|
-
return formatPromptSuccessResponse(
|
|
4391
|
-
generatedSQL,
|
|
4392
|
-
// Add references to example queries that could help
|
|
4393
|
-
dialectExamples[sqlDialect]
|
|
4394
|
-
);
|
|
4395
|
-
} catch (error) {
|
|
4396
|
-
return formatPromptErrorResponse(
|
|
4397
|
-
`Error generating SQL query schema information: ${error.message}`,
|
|
4398
|
-
"SCHEMA_RETRIEVAL_ERROR"
|
|
4399
|
-
);
|
|
4400
2847
|
}
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
);
|
|
2848
|
+
if (param.default !== void 0 || param.required === false) {
|
|
2849
|
+
fieldSchema = fieldSchema.optional();
|
|
2850
|
+
}
|
|
2851
|
+
schemaShape[param.name] = fieldSchema;
|
|
4406
2852
|
}
|
|
2853
|
+
return schemaShape;
|
|
4407
2854
|
}
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
const
|
|
4423
|
-
if (!
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
);
|
|
4428
|
-
}
|
|
4429
|
-
}
|
|
4430
|
-
const tables = await connector.getTables(schema);
|
|
4431
|
-
const normalizedTable = table?.toLowerCase() || "";
|
|
4432
|
-
const matchingTable = tables.find((t) => t.toLowerCase() === normalizedTable);
|
|
4433
|
-
if (matchingTable && table) {
|
|
4434
|
-
try {
|
|
4435
|
-
const columns = await connector.getTableSchema(matchingTable, schema);
|
|
4436
|
-
if (columns.length === 0) {
|
|
4437
|
-
return formatPromptErrorResponse(
|
|
4438
|
-
`Table '${matchingTable}' exists but has no columns or cannot be accessed.`,
|
|
4439
|
-
"EMPTY_TABLE_SCHEMA"
|
|
4440
|
-
);
|
|
4441
|
-
}
|
|
4442
|
-
const schemaInfo = schema ? ` in schema '${schema}'` : "";
|
|
4443
|
-
const tableDescription = `Table: ${matchingTable}${schemaInfo}
|
|
4444
|
-
|
|
4445
|
-
Structure:
|
|
4446
|
-
${columns.map((col) => `- ${col.column_name} (${col.data_type})${col.is_nullable === "YES" ? ", nullable" : ""}${col.column_default ? `, default: ${col.column_default}` : ""}`).join("\n")}
|
|
4447
|
-
|
|
4448
|
-
Purpose:
|
|
4449
|
-
This table appears to store ${determineTablePurpose(matchingTable, columns)}
|
|
4450
|
-
|
|
4451
|
-
Relationships:
|
|
4452
|
-
${determineRelationships(matchingTable, columns)}`;
|
|
4453
|
-
return formatPromptSuccessResponse(tableDescription);
|
|
4454
|
-
} catch (error) {
|
|
4455
|
-
return formatPromptErrorResponse(
|
|
4456
|
-
`Error retrieving schema for table '${matchingTable}': ${error.message}`,
|
|
4457
|
-
"TABLE_SCHEMA_ERROR"
|
|
4458
|
-
);
|
|
4459
|
-
}
|
|
4460
|
-
}
|
|
4461
|
-
if (table && table.includes(".")) {
|
|
4462
|
-
const [tableName, columnName] = table.split(".");
|
|
4463
|
-
const tableExists = tables.find((t) => t.toLowerCase() === tableName.toLowerCase());
|
|
4464
|
-
if (!tableExists) {
|
|
4465
|
-
return formatPromptErrorResponse(
|
|
4466
|
-
`Table '${tableName}' does not exist in schema '${schema || "default"}'. Available tables: ${tables.slice(0, 10).join(", ")}${tables.length > 10 ? "..." : ""}`,
|
|
4467
|
-
"TABLE_NOT_FOUND"
|
|
4468
|
-
);
|
|
2855
|
+
function createCustomToolHandler(toolConfig) {
|
|
2856
|
+
const zodSchemaShape = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
2857
|
+
const zodSchema = z4.object(zodSchemaShape);
|
|
2858
|
+
return async (args, extra) => {
|
|
2859
|
+
const startTime = Date.now();
|
|
2860
|
+
let success = true;
|
|
2861
|
+
let errorMessage;
|
|
2862
|
+
let paramValues = [];
|
|
2863
|
+
try {
|
|
2864
|
+
const validatedArgs = zodSchema.parse(args);
|
|
2865
|
+
const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
|
|
2866
|
+
const executeOptions = ConnectorManager.getCurrentExecuteOptions(
|
|
2867
|
+
toolConfig.source
|
|
2868
|
+
);
|
|
2869
|
+
const isReadonly = executeOptions.readonly === true;
|
|
2870
|
+
if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
|
|
2871
|
+
errorMessage = `Tool '${toolConfig.name}' cannot execute in readonly mode for source '${toolConfig.source}'. Only read-only SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
2872
|
+
success = false;
|
|
2873
|
+
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
4469
2874
|
}
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
2875
|
+
paramValues = mapArgumentsToArray(
|
|
2876
|
+
toolConfig.parameters,
|
|
2877
|
+
validatedArgs
|
|
2878
|
+
);
|
|
2879
|
+
const result = await connector.executeSQL(
|
|
2880
|
+
toolConfig.statement,
|
|
2881
|
+
executeOptions,
|
|
2882
|
+
paramValues
|
|
2883
|
+
);
|
|
2884
|
+
const responseData = {
|
|
2885
|
+
rows: result.rows,
|
|
2886
|
+
count: result.rows.length,
|
|
2887
|
+
source_id: toolConfig.source
|
|
2888
|
+
};
|
|
2889
|
+
return createToolSuccessResponse(responseData);
|
|
2890
|
+
} catch (error) {
|
|
2891
|
+
success = false;
|
|
2892
|
+
errorMessage = error.message;
|
|
2893
|
+
if (error instanceof z4.ZodError) {
|
|
2894
|
+
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
2895
|
+
errorMessage = `Parameter validation failed: ${issues}`;
|
|
2896
|
+
} else {
|
|
2897
|
+
errorMessage = `${errorMessage}
|
|
4481
2898
|
|
|
4482
|
-
|
|
4483
|
-
${
|
|
4484
|
-
return formatPromptSuccessResponse(columnDescription);
|
|
4485
|
-
} else {
|
|
4486
|
-
return formatPromptErrorResponse(
|
|
4487
|
-
`Column '${columnName}' does not exist in table '${tableName}'. Available columns: ${columns.map((c) => c.column_name).join(", ")}`,
|
|
4488
|
-
"COLUMN_NOT_FOUND"
|
|
4489
|
-
);
|
|
4490
|
-
}
|
|
4491
|
-
} catch (error) {
|
|
4492
|
-
return formatPromptErrorResponse(
|
|
4493
|
-
`Error accessing table schema: ${error.message}`,
|
|
4494
|
-
"SCHEMA_ACCESS_ERROR"
|
|
4495
|
-
);
|
|
2899
|
+
SQL: ${toolConfig.statement}
|
|
2900
|
+
Parameters: ${JSON.stringify(paramValues)}`;
|
|
4496
2901
|
}
|
|
2902
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
2903
|
+
} finally {
|
|
2904
|
+
requestStore.add({
|
|
2905
|
+
id: crypto.randomUUID(),
|
|
2906
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2907
|
+
sourceId: toolConfig.source,
|
|
2908
|
+
toolName: toolConfig.name,
|
|
2909
|
+
sql: toolConfig.statement,
|
|
2910
|
+
durationMs: Date.now() - startTime,
|
|
2911
|
+
client: getClientIdentifier(extra),
|
|
2912
|
+
success,
|
|
2913
|
+
error: errorMessage
|
|
2914
|
+
});
|
|
4497
2915
|
}
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
let dbOverview = `Database Overview ${schemaInfo}
|
|
4501
|
-
|
|
4502
|
-
Tables: ${tables.length}
|
|
4503
|
-
${tables.map((t) => `- ${t}`).join("\n")}
|
|
4504
|
-
|
|
4505
|
-
This database ${describeDatabasePurpose(tables)}`;
|
|
4506
|
-
return formatPromptSuccessResponse(dbOverview);
|
|
4507
|
-
}
|
|
4508
|
-
if (table && !normalizedTable.includes(".")) {
|
|
4509
|
-
const possibleTableMatches = tables.filter(
|
|
4510
|
-
(t) => t.toLowerCase().includes(normalizedTable) || normalizedTable.includes(t.toLowerCase())
|
|
4511
|
-
);
|
|
4512
|
-
if (possibleTableMatches.length > 0) {
|
|
4513
|
-
return formatPromptSuccessResponse(
|
|
4514
|
-
`Table "${table}" not found. Did you mean one of these tables?
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
4515
2918
|
|
|
4516
|
-
|
|
4517
|
-
|
|
2919
|
+
// src/tools/index.ts
|
|
2920
|
+
function registerTools(server) {
|
|
2921
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2922
|
+
if (sourceIds.length === 0) {
|
|
2923
|
+
throw new Error("No database sources configured");
|
|
2924
|
+
}
|
|
2925
|
+
const registry = getToolRegistry();
|
|
2926
|
+
for (const sourceId of sourceIds) {
|
|
2927
|
+
const enabledTools = registry.getEnabledToolConfigs(sourceId);
|
|
2928
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2929
|
+
const dbType = sourceConfig.type;
|
|
2930
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
2931
|
+
for (const toolConfig of enabledTools) {
|
|
2932
|
+
if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
|
|
2933
|
+
registerExecuteSqlTool(server, sourceId, dbType);
|
|
2934
|
+
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
2935
|
+
registerSearchObjectsTool(server, sourceId, dbType, isDefault);
|
|
4518
2936
|
} else {
|
|
4519
|
-
|
|
4520
|
-
return formatPromptErrorResponse(
|
|
4521
|
-
`Table "${table}" does not exist ${schemaInfo}. Available tables: ${tables.slice(0, 10).join(", ")}${tables.length > 10 ? "..." : ""}`,
|
|
4522
|
-
"TABLE_NOT_FOUND"
|
|
4523
|
-
);
|
|
2937
|
+
registerCustomTool(server, toolConfig, dbType);
|
|
4524
2938
|
}
|
|
4525
2939
|
}
|
|
4526
|
-
} catch (error) {
|
|
4527
|
-
return formatPromptErrorResponse(
|
|
4528
|
-
`Error explaining database: ${error.message}`,
|
|
4529
|
-
"EXPLANATION_ERROR"
|
|
4530
|
-
);
|
|
4531
|
-
}
|
|
4532
|
-
return formatPromptErrorResponse(
|
|
4533
|
-
`Unable to process request for schema: ${schema}, table: ${table}`,
|
|
4534
|
-
"UNHANDLED_REQUEST"
|
|
4535
|
-
);
|
|
4536
|
-
}
|
|
4537
|
-
function determineTablePurpose(tableName, columns) {
|
|
4538
|
-
const lowerTableName = tableName.toLowerCase();
|
|
4539
|
-
const columnNames = columns.map((c) => c.column_name.toLowerCase());
|
|
4540
|
-
if (lowerTableName.includes("user") || columnNames.includes("username") || columnNames.includes("email")) {
|
|
4541
|
-
return "user information and profiles";
|
|
4542
|
-
}
|
|
4543
|
-
if (lowerTableName.includes("order") || lowerTableName.includes("purchase")) {
|
|
4544
|
-
return "order or purchase transactions";
|
|
4545
2940
|
}
|
|
4546
|
-
if (lowerTableName.includes("product") || lowerTableName.includes("item")) {
|
|
4547
|
-
return "product or item information";
|
|
4548
|
-
}
|
|
4549
|
-
if (lowerTableName.includes("log") || columnNames.includes("timestamp")) {
|
|
4550
|
-
return "event or activity logs";
|
|
4551
|
-
}
|
|
4552
|
-
if (columnNames.includes("created_at") && columnNames.includes("updated_at")) {
|
|
4553
|
-
return "tracking timestamped data records";
|
|
4554
|
-
}
|
|
4555
|
-
return "data related to " + tableName;
|
|
4556
2941
|
}
|
|
4557
|
-
function
|
|
4558
|
-
const
|
|
4559
|
-
|
|
4560
|
-
|
|
2942
|
+
function registerExecuteSqlTool(server, sourceId, dbType) {
|
|
2943
|
+
const metadata = getExecuteSqlMetadata(sourceId);
|
|
2944
|
+
server.registerTool(
|
|
2945
|
+
metadata.name,
|
|
2946
|
+
{
|
|
2947
|
+
description: metadata.description,
|
|
2948
|
+
inputSchema: metadata.schema,
|
|
2949
|
+
annotations: metadata.annotations
|
|
2950
|
+
},
|
|
2951
|
+
createExecuteSqlToolHandler(sourceId)
|
|
4561
2952
|
);
|
|
4562
|
-
if (idColumns.length > 0) {
|
|
4563
|
-
idColumns.forEach((col) => {
|
|
4564
|
-
const referencedTable = col.column_name.toLowerCase().replace("_id", "");
|
|
4565
|
-
potentialRelationships.push(
|
|
4566
|
-
`May have a relationship with the "${referencedTable}" table (via ${col.column_name})`
|
|
4567
|
-
);
|
|
4568
|
-
});
|
|
4569
|
-
}
|
|
4570
|
-
if (columns.some((c) => c.column_name.toLowerCase() === "id")) {
|
|
4571
|
-
potentialRelationships.push(
|
|
4572
|
-
`May be referenced by other tables as "${tableName.toLowerCase()}_id"`
|
|
4573
|
-
);
|
|
4574
|
-
}
|
|
4575
|
-
return potentialRelationships.length > 0 ? potentialRelationships.join("\n") : "No obvious relationships identified based on column names";
|
|
4576
|
-
}
|
|
4577
|
-
function determineColumnPurpose(columnName, dataType) {
|
|
4578
|
-
const lowerColumnName = columnName.toLowerCase();
|
|
4579
|
-
if (lowerColumnName === "id") {
|
|
4580
|
-
return "Primary identifier for records in this table";
|
|
4581
|
-
}
|
|
4582
|
-
if (lowerColumnName.endsWith("_id")) {
|
|
4583
|
-
const referencedTable = lowerColumnName.replace("_id", "");
|
|
4584
|
-
return `Foreign key reference to the "${referencedTable}" table`;
|
|
4585
|
-
}
|
|
4586
|
-
if (lowerColumnName.includes("name")) {
|
|
4587
|
-
return "Stores name information";
|
|
4588
|
-
}
|
|
4589
|
-
if (lowerColumnName.includes("email")) {
|
|
4590
|
-
return "Stores email address information";
|
|
4591
|
-
}
|
|
4592
|
-
if (lowerColumnName.includes("password") || lowerColumnName.includes("hash")) {
|
|
4593
|
-
return "Stores security credential information (likely hashed)";
|
|
4594
|
-
}
|
|
4595
|
-
if (lowerColumnName === "created_at" || lowerColumnName === "created_on") {
|
|
4596
|
-
return "Timestamp for when the record was created";
|
|
4597
|
-
}
|
|
4598
|
-
if (lowerColumnName === "updated_at" || lowerColumnName === "modified_at") {
|
|
4599
|
-
return "Timestamp for when the record was last updated";
|
|
4600
|
-
}
|
|
4601
|
-
if (lowerColumnName.includes("date") || lowerColumnName.includes("time")) {
|
|
4602
|
-
return "Stores date or time information";
|
|
4603
|
-
}
|
|
4604
|
-
if (lowerColumnName.includes("price") || lowerColumnName.includes("cost") || lowerColumnName.includes("amount")) {
|
|
4605
|
-
return "Stores monetary value information";
|
|
4606
|
-
}
|
|
4607
|
-
if (dataType.includes("boolean")) {
|
|
4608
|
-
return "Stores a true/false flag";
|
|
4609
|
-
}
|
|
4610
|
-
if (dataType.includes("json")) {
|
|
4611
|
-
return "Stores structured JSON data";
|
|
4612
|
-
}
|
|
4613
|
-
if (dataType.includes("text") || dataType.includes("varchar") || dataType.includes("char")) {
|
|
4614
|
-
return "Stores text information";
|
|
4615
|
-
}
|
|
4616
|
-
return `Stores ${dataType} data`;
|
|
4617
2953
|
}
|
|
4618
|
-
function
|
|
4619
|
-
const
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
}
|
|
4635
|
-
return "contains multiple tables that store related information";
|
|
4636
|
-
}
|
|
4637
|
-
|
|
4638
|
-
// src/prompts/index.ts
|
|
4639
|
-
function registerPrompts(server) {
|
|
4640
|
-
server.prompt(
|
|
4641
|
-
"generate_sql",
|
|
4642
|
-
"Generate SQL queries from natural language descriptions",
|
|
4643
|
-
sqlGeneratorSchema,
|
|
4644
|
-
sqlGeneratorPromptHandler
|
|
2954
|
+
function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
|
|
2955
|
+
const metadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
|
|
2956
|
+
server.registerTool(
|
|
2957
|
+
metadata.name,
|
|
2958
|
+
{
|
|
2959
|
+
description: metadata.description,
|
|
2960
|
+
inputSchema: searchDatabaseObjectsSchema,
|
|
2961
|
+
annotations: {
|
|
2962
|
+
title: metadata.title,
|
|
2963
|
+
readOnlyHint: true,
|
|
2964
|
+
destructiveHint: false,
|
|
2965
|
+
idempotentHint: true,
|
|
2966
|
+
openWorldHint: false
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2969
|
+
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
4645
2970
|
);
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
2971
|
+
}
|
|
2972
|
+
function registerCustomTool(server, toolConfig, dbType) {
|
|
2973
|
+
const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
|
|
2974
|
+
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
2975
|
+
server.registerTool(
|
|
2976
|
+
toolConfig.name,
|
|
2977
|
+
{
|
|
2978
|
+
description: toolConfig.description,
|
|
2979
|
+
inputSchema: zodSchema,
|
|
2980
|
+
annotations: {
|
|
2981
|
+
title: `${toolConfig.name} (${dbType})`,
|
|
2982
|
+
readOnlyHint: isReadOnly,
|
|
2983
|
+
destructiveHint: !isReadOnly,
|
|
2984
|
+
idempotentHint: isReadOnly,
|
|
2985
|
+
openWorldHint: false
|
|
2986
|
+
}
|
|
2987
|
+
},
|
|
2988
|
+
createCustomToolHandler(toolConfig)
|
|
4651
2989
|
);
|
|
2990
|
+
console.error(` - ${toolConfig.name} \u2192 ${toolConfig.source} (${dbType})`);
|
|
4652
2991
|
}
|
|
4653
2992
|
|
|
4654
2993
|
// src/api/sources.ts
|
|
@@ -4759,11 +3098,127 @@ function listRequests(req, res) {
|
|
|
4759
3098
|
}
|
|
4760
3099
|
}
|
|
4761
3100
|
|
|
3101
|
+
// src/utils/startup-table.ts
|
|
3102
|
+
var BOX = {
|
|
3103
|
+
topLeft: "\u250C",
|
|
3104
|
+
topRight: "\u2510",
|
|
3105
|
+
bottomLeft: "\u2514",
|
|
3106
|
+
bottomRight: "\u2518",
|
|
3107
|
+
horizontal: "\u2500",
|
|
3108
|
+
vertical: "\u2502",
|
|
3109
|
+
leftT: "\u251C",
|
|
3110
|
+
rightT: "\u2524",
|
|
3111
|
+
bullet: "\u2022"
|
|
3112
|
+
};
|
|
3113
|
+
function parseHostAndDatabase(source) {
|
|
3114
|
+
if (source.dsn) {
|
|
3115
|
+
const parsed = parseConnectionInfoFromDSN(source.dsn);
|
|
3116
|
+
if (parsed) {
|
|
3117
|
+
if (parsed.type === "sqlite") {
|
|
3118
|
+
return { host: "", database: parsed.database || ":memory:" };
|
|
3119
|
+
}
|
|
3120
|
+
if (!parsed.host) {
|
|
3121
|
+
return { host: "", database: parsed.database || "" };
|
|
3122
|
+
}
|
|
3123
|
+
const port = parsed.port ?? getDefaultPortForType(parsed.type);
|
|
3124
|
+
const host2 = port ? `${parsed.host}:${port}` : parsed.host;
|
|
3125
|
+
return { host: host2, database: parsed.database || "" };
|
|
3126
|
+
}
|
|
3127
|
+
return { host: "unknown", database: "" };
|
|
3128
|
+
}
|
|
3129
|
+
const host = source.host ? source.port ? `${source.host}:${source.port}` : source.host : "";
|
|
3130
|
+
const database = source.database || "";
|
|
3131
|
+
return { host, database };
|
|
3132
|
+
}
|
|
3133
|
+
function horizontalLine(width, left, right) {
|
|
3134
|
+
return left + BOX.horizontal.repeat(width - 2) + right;
|
|
3135
|
+
}
|
|
3136
|
+
function fitString(str, width) {
|
|
3137
|
+
if (str.length > width) {
|
|
3138
|
+
return str.slice(0, width - 1) + "\u2026";
|
|
3139
|
+
}
|
|
3140
|
+
return str.padEnd(width);
|
|
3141
|
+
}
|
|
3142
|
+
function formatHostDatabase(host, database) {
|
|
3143
|
+
return host ? database ? `${host}/${database}` : host : database || "";
|
|
3144
|
+
}
|
|
3145
|
+
function generateStartupTable(sources) {
|
|
3146
|
+
if (sources.length === 0) {
|
|
3147
|
+
return "";
|
|
3148
|
+
}
|
|
3149
|
+
const idTypeWidth = Math.max(
|
|
3150
|
+
20,
|
|
3151
|
+
...sources.map((s) => `${s.id} (${s.type})`.length)
|
|
3152
|
+
);
|
|
3153
|
+
const hostDbWidth = Math.max(
|
|
3154
|
+
24,
|
|
3155
|
+
...sources.map((s) => formatHostDatabase(s.host, s.database).length)
|
|
3156
|
+
);
|
|
3157
|
+
const modeWidth = Math.max(
|
|
3158
|
+
10,
|
|
3159
|
+
...sources.map((s) => {
|
|
3160
|
+
const modes = [];
|
|
3161
|
+
if (s.isDemo) modes.push("DEMO");
|
|
3162
|
+
if (s.readonly) modes.push("READ-ONLY");
|
|
3163
|
+
return modes.join(" ").length;
|
|
3164
|
+
})
|
|
3165
|
+
);
|
|
3166
|
+
const totalWidth = 2 + idTypeWidth + 3 + hostDbWidth + 3 + modeWidth + 2;
|
|
3167
|
+
const lines = [];
|
|
3168
|
+
for (let i = 0; i < sources.length; i++) {
|
|
3169
|
+
const source = sources[i];
|
|
3170
|
+
const isFirst = i === 0;
|
|
3171
|
+
const isLast = i === sources.length - 1;
|
|
3172
|
+
if (isFirst) {
|
|
3173
|
+
lines.push(horizontalLine(totalWidth, BOX.topLeft, BOX.topRight));
|
|
3174
|
+
}
|
|
3175
|
+
const idType = fitString(`${source.id} (${source.type})`, idTypeWidth);
|
|
3176
|
+
const hostDb = fitString(
|
|
3177
|
+
formatHostDatabase(source.host, source.database),
|
|
3178
|
+
hostDbWidth
|
|
3179
|
+
);
|
|
3180
|
+
const modes = [];
|
|
3181
|
+
if (source.isDemo) modes.push("DEMO");
|
|
3182
|
+
if (source.readonly) modes.push("READ-ONLY");
|
|
3183
|
+
const modeStr = fitString(modes.join(" "), modeWidth);
|
|
3184
|
+
lines.push(
|
|
3185
|
+
`${BOX.vertical} ${idType} ${BOX.vertical} ${hostDb} ${BOX.vertical} ${modeStr} ${BOX.vertical}`
|
|
3186
|
+
);
|
|
3187
|
+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
|
|
3188
|
+
for (const tool of source.tools) {
|
|
3189
|
+
const toolLine = ` ${BOX.bullet} ${tool}`;
|
|
3190
|
+
lines.push(
|
|
3191
|
+
`${BOX.vertical} ${fitString(toolLine, totalWidth - 4)} ${BOX.vertical}`
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
if (isLast) {
|
|
3195
|
+
lines.push(horizontalLine(totalWidth, BOX.bottomLeft, BOX.bottomRight));
|
|
3196
|
+
} else {
|
|
3197
|
+
lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
return lines.join("\n");
|
|
3201
|
+
}
|
|
3202
|
+
function buildSourceDisplayInfo(sourceConfigs, getToolsForSource2, isDemo) {
|
|
3203
|
+
return sourceConfigs.map((source) => {
|
|
3204
|
+
const { host, database } = parseHostAndDatabase(source);
|
|
3205
|
+
return {
|
|
3206
|
+
id: source.id,
|
|
3207
|
+
type: source.type || "sqlite",
|
|
3208
|
+
host,
|
|
3209
|
+
database,
|
|
3210
|
+
readonly: source.readonly || false,
|
|
3211
|
+
isDemo,
|
|
3212
|
+
tools: getToolsForSource2(source.id)
|
|
3213
|
+
};
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
|
|
4762
3217
|
// src/server.ts
|
|
4763
|
-
var
|
|
4764
|
-
var
|
|
4765
|
-
var packageJsonPath =
|
|
4766
|
-
var packageJson = JSON.parse(
|
|
3218
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
3219
|
+
var __dirname = path.dirname(__filename);
|
|
3220
|
+
var packageJsonPath = path.join(__dirname, "..", "package.json");
|
|
3221
|
+
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
4767
3222
|
var SERVER_NAME = "DBHub MCP Server";
|
|
4768
3223
|
var SERVER_VERSION = packageJson.version;
|
|
4769
3224
|
function generateBanner(version, modes = []) {
|
|
@@ -4776,7 +3231,7 @@ function generateBanner(version, modes = []) {
|
|
|
4776
3231
|
| |__| | |_) | | | | |_| | |_) |
|
|
4777
3232
|
|_____/|____/|_| |_|\\__,_|_.__/
|
|
4778
3233
|
|
|
4779
|
-
v${version}${modeText} -
|
|
3234
|
+
v${version}${modeText} - Minimal Database MCP Server
|
|
4780
3235
|
`;
|
|
4781
3236
|
}
|
|
4782
3237
|
async function main() {
|
|
@@ -4807,16 +3262,6 @@ See documentation for more details on configuring database connections.
|
|
|
4807
3262
|
`);
|
|
4808
3263
|
process.exit(1);
|
|
4809
3264
|
}
|
|
4810
|
-
const createServer2 = () => {
|
|
4811
|
-
const server = new McpServer2({
|
|
4812
|
-
name: SERVER_NAME,
|
|
4813
|
-
version: SERVER_VERSION
|
|
4814
|
-
});
|
|
4815
|
-
registerResources(server);
|
|
4816
|
-
registerTools(server);
|
|
4817
|
-
registerPrompts(server);
|
|
4818
|
-
return server;
|
|
4819
|
-
};
|
|
4820
3265
|
const connectorManager = new ConnectorManager();
|
|
4821
3266
|
const sources = sourceConfigsData.sources;
|
|
4822
3267
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
@@ -4826,13 +3271,22 @@ See documentation for more details on configuring database connections.
|
|
|
4826
3271
|
console.error(` - ${source.id}: ${redactDSN(dsn)}`);
|
|
4827
3272
|
}
|
|
4828
3273
|
await connectorManager.connectWithSources(sources);
|
|
3274
|
+
const { initializeToolRegistry } = await import("./registry-AWAIN6WO.js");
|
|
3275
|
+
initializeToolRegistry({
|
|
3276
|
+
sources: sourceConfigsData.sources,
|
|
3277
|
+
tools: sourceConfigsData.tools
|
|
3278
|
+
});
|
|
3279
|
+
console.error("Tool registry initialized");
|
|
3280
|
+
const createServer = () => {
|
|
3281
|
+
const server = new McpServer({
|
|
3282
|
+
name: SERVER_NAME,
|
|
3283
|
+
version: SERVER_VERSION
|
|
3284
|
+
});
|
|
3285
|
+
registerTools(server);
|
|
3286
|
+
return server;
|
|
3287
|
+
};
|
|
4829
3288
|
const transportData = resolveTransport();
|
|
4830
|
-
|
|
4831
|
-
const portData = transportData.type === "http" ? resolvePort() : null;
|
|
4832
|
-
if (portData) {
|
|
4833
|
-
console.error(`HTTP server port: ${portData.port} (source: ${portData.source})`);
|
|
4834
|
-
}
|
|
4835
|
-
const readonly = isReadOnlyMode();
|
|
3289
|
+
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
4836
3290
|
const activeModes = [];
|
|
4837
3291
|
const modeDescriptions = [];
|
|
4838
3292
|
const isDemo = isDemoMode();
|
|
@@ -4840,19 +3294,17 @@ See documentation for more details on configuring database connections.
|
|
|
4840
3294
|
activeModes.push("DEMO");
|
|
4841
3295
|
modeDescriptions.push("using sample employee database");
|
|
4842
3296
|
}
|
|
4843
|
-
if (readonly) {
|
|
4844
|
-
activeModes.push("READ-ONLY");
|
|
4845
|
-
modeDescriptions.push("only read only queries allowed");
|
|
4846
|
-
}
|
|
4847
|
-
if (sources.length > 1) {
|
|
4848
|
-
console.error(`Multi-source mode: ${sources.length} databases configured`);
|
|
4849
|
-
}
|
|
4850
3297
|
if (activeModes.length > 0) {
|
|
4851
3298
|
console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
|
|
4852
3299
|
}
|
|
4853
3300
|
console.error(generateBanner(SERVER_VERSION, activeModes));
|
|
3301
|
+
const sourceDisplayInfos = buildSourceDisplayInfo(
|
|
3302
|
+
sources,
|
|
3303
|
+
(sourceId) => getToolsForSource(sourceId).map((t) => t.name),
|
|
3304
|
+
isDemo
|
|
3305
|
+
);
|
|
3306
|
+
console.error(generateStartupTable(sourceDisplayInfos));
|
|
4854
3307
|
if (transportData.type === "http") {
|
|
4855
|
-
const port = portData.port;
|
|
4856
3308
|
const app = express();
|
|
4857
3309
|
app.use(express.json());
|
|
4858
3310
|
app.use((req, res, next) => {
|
|
@@ -4869,7 +3321,7 @@ See documentation for more details on configuring database connections.
|
|
|
4869
3321
|
}
|
|
4870
3322
|
next();
|
|
4871
3323
|
});
|
|
4872
|
-
const frontendPath =
|
|
3324
|
+
const frontendPath = path.join(__dirname, "public");
|
|
4873
3325
|
app.use(express.static(frontendPath));
|
|
4874
3326
|
app.get("/healthz", (req, res) => {
|
|
4875
3327
|
res.status(200).send("OK");
|
|
@@ -4891,7 +3343,7 @@ See documentation for more details on configuring database connections.
|
|
|
4891
3343
|
enableJsonResponse: true
|
|
4892
3344
|
// Use JSON responses (SSE not supported in stateless mode)
|
|
4893
3345
|
});
|
|
4894
|
-
const server =
|
|
3346
|
+
const server = createServer();
|
|
4895
3347
|
await server.connect(transport);
|
|
4896
3348
|
await transport.handleRequest(req, res, req.body);
|
|
4897
3349
|
} catch (error) {
|
|
@@ -4903,7 +3355,7 @@ See documentation for more details on configuring database connections.
|
|
|
4903
3355
|
});
|
|
4904
3356
|
if (process.env.NODE_ENV !== "development") {
|
|
4905
3357
|
app.get("*", (req, res) => {
|
|
4906
|
-
res.sendFile(
|
|
3358
|
+
res.sendFile(path.join(frontendPath, "index.html"));
|
|
4907
3359
|
});
|
|
4908
3360
|
}
|
|
4909
3361
|
app.listen(port, "0.0.0.0", () => {
|
|
@@ -4918,7 +3370,7 @@ See documentation for more details on configuring database connections.
|
|
|
4918
3370
|
console.error(`MCP server endpoint at http://0.0.0.0:${port}/mcp`);
|
|
4919
3371
|
});
|
|
4920
3372
|
} else {
|
|
4921
|
-
const server =
|
|
3373
|
+
const server = createServer();
|
|
4922
3374
|
const transport = new StdioServerTransport();
|
|
4923
3375
|
await server.connect(transport);
|
|
4924
3376
|
console.error("MCP server running on stdio");
|