@bytebase/dbhub 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,262 +1,33 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ getToolRegistry
4
+ } from "./chunk-VWZF5OAJ.js";
5
+ import {
6
+ ConnectorManager,
7
+ ConnectorRegistry,
8
+ SafeURL,
9
+ buildDSNFromSource,
10
+ customToolRegistry,
11
+ getDatabaseTypeFromDSN,
12
+ getDefaultPortForType,
13
+ isDemoMode,
14
+ mapArgumentsToArray,
15
+ obfuscateDSNPassword,
16
+ parseConnectionInfoFromDSN,
17
+ redactDSN,
18
+ resolvePort,
19
+ resolveSourceConfigs,
20
+ resolveTransport,
21
+ stripCommentsAndStrings
22
+ } from "./chunk-WSLDVMBA.js";
23
+ import {
24
+ BUILTIN_TOOL_EXECUTE_SQL,
25
+ BUILTIN_TOOL_SEARCH_OBJECTS
26
+ } from "./chunk-D23WQQY7.js";
2
27
 
3
28
  // src/connectors/postgres/index.ts
4
29
  import pg from "pg";
5
30
 
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
31
  // src/utils/sql-row-limiter.ts
261
32
  var SQLRowLimiter = class {
262
33
  /**
@@ -268,34 +39,42 @@ var SQLRowLimiter = class {
268
39
  return trimmed.startsWith("select");
269
40
  }
270
41
  /**
271
- * Check if a SQL statement already has a LIMIT clause
42
+ * Check if a SQL statement already has a LIMIT clause.
43
+ * Strips comments and string literals first to avoid false positives.
272
44
  */
273
45
  static hasLimitClause(sql2) {
274
- const limitRegex = /\blimit\s+\d+/i;
275
- return limitRegex.test(sql2);
46
+ const cleanedSQL = stripCommentsAndStrings(sql2);
47
+ const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
48
+ return limitRegex.test(cleanedSQL);
276
49
  }
277
50
  /**
278
- * Check if a SQL statement already has a TOP clause (SQL Server)
51
+ * Check if a SQL statement already has a TOP clause (SQL Server).
52
+ * Strips comments and string literals first to avoid false positives.
279
53
  */
280
54
  static hasTopClause(sql2) {
55
+ const cleanedSQL = stripCommentsAndStrings(sql2);
281
56
  const topRegex = /\bselect\s+top\s+\d+/i;
282
- return topRegex.test(sql2);
57
+ return topRegex.test(cleanedSQL);
283
58
  }
284
59
  /**
285
- * Extract existing LIMIT value from SQL if present
60
+ * Extract existing LIMIT value from SQL if present.
61
+ * Strips comments and string literals first to avoid false positives.
286
62
  */
287
63
  static extractLimitValue(sql2) {
288
- const limitMatch = sql2.match(/\blimit\s+(\d+)/i);
64
+ const cleanedSQL = stripCommentsAndStrings(sql2);
65
+ const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
289
66
  if (limitMatch) {
290
67
  return parseInt(limitMatch[1], 10);
291
68
  }
292
69
  return null;
293
70
  }
294
71
  /**
295
- * Extract existing TOP value from SQL if present (SQL Server)
72
+ * Extract existing TOP value from SQL if present (SQL Server).
73
+ * Strips comments and string literals first to avoid false positives.
296
74
  */
297
75
  static extractTopValue(sql2) {
298
- const topMatch = sql2.match(/\bselect\s+top\s+(\d+)/i);
76
+ const cleanedSQL = stripCommentsAndStrings(sql2);
77
+ const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
299
78
  if (topMatch) {
300
79
  return parseInt(topMatch[1], 10);
301
80
  }
@@ -328,13 +107,34 @@ var SQLRowLimiter = class {
328
107
  return sql2.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
329
108
  }
330
109
  }
110
+ /**
111
+ * Check if a LIMIT clause uses a parameter placeholder (not a literal number).
112
+ * Strips comments and string literals first to avoid false positives.
113
+ */
114
+ static hasParameterizedLimit(sql2) {
115
+ const cleanedSQL = stripCommentsAndStrings(sql2);
116
+ const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
117
+ return parameterizedLimitRegex.test(cleanedSQL);
118
+ }
331
119
  /**
332
120
  * Apply maxRows limit to a SELECT query only
121
+ *
122
+ * This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
123
+ * the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
124
+ *
125
+ * For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
126
+ * to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
333
127
  */
334
128
  static applyMaxRows(sql2, maxRows) {
335
129
  if (!maxRows || !this.isSelectQuery(sql2)) {
336
130
  return sql2;
337
131
  }
132
+ if (this.hasParameterizedLimit(sql2)) {
133
+ const trimmed = sql2.trim();
134
+ const hasSemicolon = trimmed.endsWith(";");
135
+ const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
136
+ return `SELECT * FROM (${sqlWithoutSemicolon}) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
137
+ }
338
138
  return this.applyLimitToQuery(sql2, maxRows);
339
139
  }
340
140
  /**
@@ -410,6 +210,11 @@ var PostgresConnector = class _PostgresConnector {
410
210
  this.name = "PostgreSQL";
411
211
  this.dsnParser = new PostgresDSNParser();
412
212
  this.pool = null;
213
+ // Source ID is set by ConnectorManager after cloning
214
+ this.sourceId = "default";
215
+ }
216
+ getId() {
217
+ return this.sourceId;
413
218
  }
414
219
  clone() {
415
220
  return new _PostgresConnector();
@@ -422,7 +227,6 @@ var PostgresConnector = class _PostgresConnector {
422
227
  }
423
228
  this.pool = new Pool(poolConfig);
424
229
  const client = await this.pool.connect();
425
- console.error("Successfully connected to PostgreSQL database");
426
230
  client.release();
427
231
  } catch (err) {
428
232
  console.error("Failed to connect to PostgreSQL database:", err);
@@ -667,7 +471,7 @@ var PostgresConnector = class _PostgresConnector {
667
471
  client.release();
668
472
  }
669
473
  }
670
- async executeSQL(sql2, options) {
474
+ async executeSQL(sql2, options, parameters) {
671
475
  if (!this.pool) {
672
476
  throw new Error("Not connected to database");
673
477
  }
@@ -676,8 +480,21 @@ var PostgresConnector = class _PostgresConnector {
676
480
  const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
677
481
  if (statements.length === 1) {
678
482
  const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
483
+ if (parameters && parameters.length > 0) {
484
+ try {
485
+ return await client.query(processedStatement, parameters);
486
+ } catch (error) {
487
+ console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
488
+ console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
489
+ console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
490
+ throw error;
491
+ }
492
+ }
679
493
  return await client.query(processedStatement);
680
494
  } else {
495
+ if (parameters && parameters.length > 0) {
496
+ throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
497
+ }
681
498
  let allRows = [];
682
499
  await client.query("BEGIN");
683
500
  try {
@@ -800,6 +617,11 @@ var SQLServerConnector = class _SQLServerConnector {
800
617
  this.id = "sqlserver";
801
618
  this.name = "SQL Server";
802
619
  this.dsnParser = new SQLServerDSNParser();
620
+ // Source ID is set by ConnectorManager after cloning
621
+ this.sourceId = "default";
622
+ }
623
+ getId() {
624
+ return this.sourceId;
803
625
  }
804
626
  clone() {
805
627
  return new _SQLServerConnector();
@@ -1042,16 +864,49 @@ var SQLServerConnector = class _SQLServerConnector {
1042
864
  throw new Error(`Failed to get stored procedure details: ${error.message}`);
1043
865
  }
1044
866
  }
1045
- async executeSQL(sql2, options) {
867
+ async executeSQL(sqlQuery, options, parameters) {
1046
868
  if (!this.connection) {
1047
869
  throw new Error("Not connected to SQL Server database");
1048
870
  }
1049
871
  try {
1050
- let processedSQL = sql2;
872
+ let processedSQL = sqlQuery;
1051
873
  if (options.maxRows) {
1052
- processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sql2, options.maxRows);
874
+ processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
875
+ }
876
+ const request = this.connection.request();
877
+ if (parameters && parameters.length > 0) {
878
+ parameters.forEach((param, index) => {
879
+ const paramName = `p${index + 1}`;
880
+ if (typeof param === "string") {
881
+ request.input(paramName, sql.VarChar, param);
882
+ } else if (typeof param === "number") {
883
+ if (Number.isInteger(param)) {
884
+ request.input(paramName, sql.Int, param);
885
+ } else {
886
+ request.input(paramName, sql.Float, param);
887
+ }
888
+ } else if (typeof param === "boolean") {
889
+ request.input(paramName, sql.Bit, param);
890
+ } else if (param === null || param === void 0) {
891
+ request.input(paramName, sql.VarChar, param);
892
+ } else if (Array.isArray(param)) {
893
+ request.input(paramName, sql.VarChar, JSON.stringify(param));
894
+ } else {
895
+ request.input(paramName, sql.VarChar, JSON.stringify(param));
896
+ }
897
+ });
898
+ }
899
+ let result;
900
+ try {
901
+ result = await request.query(processedSQL);
902
+ } catch (error) {
903
+ if (parameters && parameters.length > 0) {
904
+ console.error(`[SQL Server executeSQL] ERROR: ${error.message}`);
905
+ console.error(`[SQL Server executeSQL] SQL: ${processedSQL}`);
906
+ console.error(`[SQL Server executeSQL] Parameters: ${JSON.stringify(parameters)}`);
907
+ }
908
+ throw error;
1053
909
  }
1054
- const result = await this.connection.request().query(processedSQL);
1055
910
  return {
1056
911
  rows: result.recordset || [],
1057
912
  fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
@@ -1151,8 +1006,13 @@ var SQLiteConnector = class _SQLiteConnector {
1151
1006
  this.dsnParser = new SQLiteDSNParser();
1152
1007
  this.db = null;
1153
1008
  this.dbPath = ":memory:";
1009
+ // Default to in-memory database
1010
+ // Source ID is set by ConnectorManager after cloning
1011
+ this.sourceId = "default";
1012
+ }
1013
+ getId() {
1014
+ return this.sourceId;
1154
1015
  }
1155
- // Default to in-memory database
1156
1016
  clone() {
1157
1017
  return new _SQLiteConnector();
1158
1018
  }
@@ -1170,10 +1030,8 @@ var SQLiteConnector = class _SQLiteConnector {
1170
1030
  dbOptions.readonly = true;
1171
1031
  }
1172
1032
  this.db = new Database(this.dbPath, dbOptions);
1173
- console.error("Successfully connected to SQLite database");
1174
1033
  if (initScript) {
1175
1034
  this.db.exec(initScript);
1176
- console.error("Successfully initialized database with script");
1177
1035
  }
1178
1036
  } catch (error) {
1179
1037
  console.error("Failed to connect to SQLite database:", error);
@@ -1320,7 +1178,7 @@ var SQLiteConnector = class _SQLiteConnector {
1320
1178
  "SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
1321
1179
  );
1322
1180
  }
1323
- async executeSQL(sql2, options) {
1181
+ async executeSQL(sql2, options, parameters) {
1324
1182
  if (!this.db) {
1325
1183
  throw new Error("Not connected to SQLite database");
1326
1184
  }
@@ -1334,13 +1192,39 @@ var SQLiteConnector = class _SQLiteConnector {
1334
1192
  processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows);
1335
1193
  }
1336
1194
  if (isReadStatement) {
1337
- const rows = this.db.prepare(processedStatement).all();
1338
- return { rows };
1195
+ if (parameters && parameters.length > 0) {
1196
+ try {
1197
+ const rows = this.db.prepare(processedStatement).all(...parameters);
1198
+ return { rows };
1199
+ } catch (error) {
1200
+ console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
1201
+ console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
1202
+ console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
1203
+ throw error;
1204
+ }
1205
+ } else {
1206
+ const rows = this.db.prepare(processedStatement).all();
1207
+ return { rows };
1208
+ }
1339
1209
  } else {
1340
- this.db.prepare(processedStatement).run();
1210
+ if (parameters && parameters.length > 0) {
1211
+ try {
1212
+ this.db.prepare(processedStatement).run(...parameters);
1213
+ } catch (error) {
1214
+ console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
1215
+ console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
1216
+ console.error(`[SQLite executeSQL] Parameters: ${JSON.stringify(parameters)}`);
1217
+ throw error;
1218
+ }
1219
+ } else {
1220
+ this.db.prepare(processedStatement).run();
1221
+ }
1341
1222
  return { rows: [] };
1342
1223
  }
1343
1224
  } else {
1225
+ if (parameters && parameters.length > 0) {
1226
+ throw new Error("Parameters are not supported for multi-statement queries in SQLite");
1227
+ }
1344
1228
  const readStatements = [];
1345
1229
  const writeStatements = [];
1346
1230
  for (const statement of statements) {
@@ -1472,6 +1356,11 @@ var MySQLConnector = class _MySQLConnector {
1472
1356
  this.name = "MySQL";
1473
1357
  this.dsnParser = new MySQLDSNParser();
1474
1358
  this.pool = null;
1359
+ // Source ID is set by ConnectorManager after cloning
1360
+ this.sourceId = "default";
1361
+ }
1362
+ getId() {
1363
+ return this.sourceId;
1475
1364
  }
1476
1365
  clone() {
1477
1366
  return new _MySQLConnector();
@@ -1481,7 +1370,6 @@ var MySQLConnector = class _MySQLConnector {
1481
1370
  const connectionOptions = await this.dsnParser.parse(dsn, config);
1482
1371
  this.pool = mysql.createPool(connectionOptions);
1483
1372
  const [rows] = await this.pool.query("SELECT 1");
1484
- console.error("Successfully connected to MySQL database");
1485
1373
  } catch (err) {
1486
1374
  console.error("Failed to connect to MySQL database:", err);
1487
1375
  throw err;
@@ -1763,7 +1651,7 @@ var MySQLConnector = class _MySQLConnector {
1763
1651
  const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
1764
1652
  return rows[0].DB;
1765
1653
  }
1766
- async executeSQL(sql2, options) {
1654
+ async executeSQL(sql2, options, parameters) {
1767
1655
  if (!this.pool) {
1768
1656
  throw new Error("Not connected to database");
1769
1657
  }
@@ -1780,7 +1668,19 @@ var MySQLConnector = class _MySQLConnector {
1780
1668
  processedSQL += ";";
1781
1669
  }
1782
1670
  }
1783
- const results = await conn.query(processedSQL);
1671
+ let results;
1672
+ if (parameters && parameters.length > 0) {
1673
+ try {
1674
+ results = await conn.query(processedSQL, parameters);
1675
+ } catch (error) {
1676
+ console.error(`[MySQL executeSQL] ERROR: ${error.message}`);
1677
+ console.error(`[MySQL executeSQL] SQL: ${processedSQL}`);
1678
+ console.error(`[MySQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
1679
+ throw error;
1680
+ }
1681
+ } else {
1682
+ results = await conn.query(processedSQL);
1683
+ }
1784
1684
  const [firstResult] = results;
1785
1685
  const rows = parseQueryResults(firstResult);
1786
1686
  return { rows };
@@ -1859,6 +1759,11 @@ var MariaDBConnector = class _MariaDBConnector {
1859
1759
  this.name = "MariaDB";
1860
1760
  this.dsnParser = new MariadbDSNParser();
1861
1761
  this.pool = null;
1762
+ // Source ID is set by ConnectorManager after cloning
1763
+ this.sourceId = "default";
1764
+ }
1765
+ getId() {
1766
+ return this.sourceId;
1862
1767
  }
1863
1768
  clone() {
1864
1769
  return new _MariaDBConnector();
@@ -1867,9 +1772,7 @@ var MariaDBConnector = class _MariaDBConnector {
1867
1772
  try {
1868
1773
  const connectionConfig = await this.dsnParser.parse(dsn, config);
1869
1774
  this.pool = mariadb.createPool(connectionConfig);
1870
- console.error("Testing connection to MariaDB...");
1871
1775
  await this.pool.query("SELECT 1");
1872
- console.error("Successfully connected to MariaDB database");
1873
1776
  } catch (err) {
1874
1777
  console.error("Failed to connect to MariaDB database:", err);
1875
1778
  throw err;
@@ -2151,7 +2054,7 @@ var MariaDBConnector = class _MariaDBConnector {
2151
2054
  const rows = await this.pool.query("SELECT DATABASE() AS DB");
2152
2055
  return rows[0].DB;
2153
2056
  }
2154
- async executeSQL(sql2, options) {
2057
+ async executeSQL(sql2, options, parameters) {
2155
2058
  if (!this.pool) {
2156
2059
  throw new Error("Not connected to database");
2157
2060
  }
@@ -2168,7 +2071,19 @@ var MariaDBConnector = class _MariaDBConnector {
2168
2071
  processedSQL += ";";
2169
2072
  }
2170
2073
  }
2171
- const results = await conn.query(processedSQL);
2074
+ let results;
2075
+ if (parameters && parameters.length > 0) {
2076
+ try {
2077
+ results = await conn.query(processedSQL, parameters);
2078
+ } catch (error) {
2079
+ console.error(`[MariaDB executeSQL] ERROR: ${error.message}`);
2080
+ console.error(`[MariaDB executeSQL] SQL: ${processedSQL}`);
2081
+ console.error(`[MariaDB executeSQL] Parameters: ${JSON.stringify(parameters)}`);
2082
+ throw error;
2083
+ }
2084
+ } else {
2085
+ results = await conn.query(processedSQL);
2086
+ }
2172
2087
  const rows = parseQueryResults(results);
2173
2088
  return { rows };
2174
2089
  } catch (error) {
@@ -2183,1530 +2098,132 @@ var mariadbConnector = new MariaDBConnector();
2183
2098
  ConnectorRegistry.register(mariadbConnector);
2184
2099
 
2185
2100
  // src/server.ts
2186
- import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
2101
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2187
2102
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2188
2103
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2189
2104
  import express from "express";
2190
- import path3 from "path";
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";
2105
+ import path from "path";
2196
2106
  import { readFileSync } from "fs";
2197
- import { createServer } from "net";
2198
- var SSHTunnel = class {
2199
- constructor() {
2200
- this.sshClient = null;
2201
- this.localServer = null;
2202
- this.tunnelInfo = null;
2203
- this.isConnected = false;
2107
+ import { fileURLToPath } from "url";
2108
+
2109
+ // src/tools/execute-sql.ts
2110
+ import { z } from "zod";
2111
+
2112
+ // src/utils/response-formatter.ts
2113
+ function bigIntReplacer(_key, value) {
2114
+ if (typeof value === "bigint") {
2115
+ return value.toString();
2204
2116
  }
2205
- /**
2206
- * Establish an SSH tunnel
2207
- * @param config SSH connection configuration
2208
- * @param options Tunnel options including target host and port
2209
- * @returns Promise resolving to tunnel information including local port
2210
- */
2211
- async establish(config, options) {
2212
- if (this.isConnected) {
2213
- throw new Error("SSH tunnel is already established");
2214
- }
2215
- return new Promise((resolve, reject) => {
2216
- this.sshClient = new Client();
2217
- const sshConfig = {
2218
- host: config.host,
2219
- port: config.port || 22,
2220
- username: config.username
2221
- };
2222
- if (config.password) {
2223
- sshConfig.password = config.password;
2224
- } else if (config.privateKey) {
2225
- try {
2226
- const privateKey = readFileSync(config.privateKey);
2227
- sshConfig.privateKey = privateKey;
2228
- if (config.passphrase) {
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;
2117
+ return value;
2118
+ }
2119
+ function formatSuccessResponse(data, meta = {}) {
2120
+ return {
2121
+ success: true,
2122
+ data,
2123
+ ...Object.keys(meta).length > 0 ? { meta } : {}
2124
+ };
2125
+ }
2126
+ function formatErrorResponse(error, code = "ERROR", details) {
2127
+ return {
2128
+ success: false,
2129
+ error,
2130
+ code,
2131
+ ...details ? { details } : {}
2132
+ };
2133
+ }
2134
+ function createToolErrorResponse(error, code = "ERROR", details) {
2135
+ return {
2136
+ content: [
2137
+ {
2138
+ type: "text",
2139
+ text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
2140
+ mimeType: "application/json"
2238
2141
  }
2239
- this.sshClient.on("error", (err) => {
2240
- this.cleanup();
2241
- reject(new Error(`SSH connection error: ${err.message}`));
2242
- });
2243
- this.sshClient.on("ready", () => {
2244
- console.error("SSH connection established");
2245
- this.localServer = createServer((localSocket) => {
2246
- this.sshClient.forwardOut(
2247
- "127.0.0.1",
2248
- 0,
2249
- options.targetHost,
2250
- options.targetPort,
2251
- (err, stream) => {
2252
- if (err) {
2253
- console.error("SSH forward error:", err);
2254
- localSocket.end();
2255
- return;
2256
- }
2257
- localSocket.pipe(stream).pipe(localSocket);
2258
- stream.on("error", (err2) => {
2259
- console.error("SSH stream error:", err2);
2260
- localSocket.end();
2261
- });
2262
- localSocket.on("error", (err2) => {
2263
- console.error("Local socket error:", err2);
2264
- stream.end();
2265
- });
2266
- }
2267
- );
2268
- });
2269
- const localPort = options.localPort || 0;
2270
- this.localServer.listen(localPort, "127.0.0.1", () => {
2271
- const address = this.localServer.address();
2272
- if (!address || typeof address === "string") {
2273
- this.cleanup();
2274
- reject(new Error("Failed to get local server address"));
2275
- return;
2276
- }
2277
- this.tunnelInfo = {
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
- });
2142
+ ],
2143
+ isError: true
2144
+ };
2145
+ }
2146
+ function createToolSuccessResponse(data, meta = {}) {
2147
+ return {
2148
+ content: [
2149
+ {
2150
+ type: "text",
2151
+ text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
2152
+ mimeType: "application/json"
2153
+ }
2154
+ ]
2155
+ };
2156
+ }
2157
+
2158
+ // src/utils/allowed-keywords.ts
2159
+ var allowedKeywords = {
2160
+ postgres: ["select", "with", "explain", "analyze", "show"],
2161
+ mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2162
+ mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2163
+ sqlite: ["select", "with", "explain", "analyze", "pragma"],
2164
+ sqlserver: ["select", "with", "explain", "showplan"]
2165
+ };
2166
+ function isReadOnlySQL(sql2, connectorType) {
2167
+ const cleanedSQL = stripCommentsAndStrings(sql2).trim().toLowerCase();
2168
+ if (!cleanedSQL) {
2169
+ return true;
2170
+ }
2171
+ const firstWord = cleanedSQL.split(/\s+/)[0];
2172
+ const keywordList = allowedKeywords[connectorType] || [];
2173
+ return keywordList.includes(firstWord);
2174
+ }
2175
+
2176
+ // src/requests/store.ts
2177
+ var RequestStore = class {
2178
+ constructor() {
2179
+ this.store = /* @__PURE__ */ new Map();
2180
+ this.maxPerSource = 100;
2293
2181
  }
2294
2182
  /**
2295
- * Close the SSH tunnel and clean up resources
2183
+ * Add a request to the store
2184
+ * Evicts oldest entry if at capacity
2296
2185
  */
2297
- async close() {
2298
- if (!this.isConnected) {
2299
- return;
2186
+ add(request) {
2187
+ const requests = this.store.get(request.sourceId) ?? [];
2188
+ requests.push(request);
2189
+ if (requests.length > this.maxPerSource) {
2190
+ requests.shift();
2300
2191
  }
2301
- return new Promise((resolve) => {
2302
- this.cleanup();
2303
- this.isConnected = false;
2304
- console.error("SSH tunnel closed");
2305
- resolve();
2306
- });
2192
+ this.store.set(request.sourceId, requests);
2307
2193
  }
2308
2194
  /**
2309
- * Clean up resources
2195
+ * Get requests, optionally filtered by source
2196
+ * Returns newest first
2310
2197
  */
2311
- cleanup() {
2312
- if (this.localServer) {
2313
- this.localServer.close();
2314
- this.localServer = null;
2315
- }
2316
- if (this.sshClient) {
2317
- this.sshClient.end();
2318
- this.sshClient = null;
2198
+ getAll(sourceId) {
2199
+ let requests;
2200
+ if (sourceId) {
2201
+ requests = [...this.store.get(sourceId) ?? []];
2202
+ } else {
2203
+ requests = Array.from(this.store.values()).flat();
2319
2204
  }
2320
- this.tunnelInfo = null;
2205
+ return requests.sort(
2206
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
2207
+ );
2321
2208
  }
2322
2209
  /**
2323
- * Get current tunnel information
2210
+ * Get total count of requests across all sources
2324
2211
  */
2325
- getTunnelInfo() {
2326
- return this.tunnelInfo;
2212
+ getTotal() {
2213
+ return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
2327
2214
  }
2328
2215
  /**
2329
- * Check if tunnel is connected
2216
+ * Clear all requests (useful for testing)
2330
2217
  */
2331
- getIsConnected() {
2332
- return this.isConnected;
2218
+ clear() {
2219
+ this.store.clear();
2333
2220
  }
2334
2221
  };
2335
2222
 
2336
- // src/config/toml-loader.ts
2337
- import fs2 from "fs";
2338
- import path2 from "path";
2339
- import { homedir as homedir3 } from "os";
2340
- import toml from "@iarna/toml";
2223
+ // src/requests/index.ts
2224
+ var requestStore = new RequestStore();
2341
2225
 
2342
- // src/config/env.ts
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
2226
+ // src/utils/client-identifier.ts
3710
2227
  function getClientIdentifier(extra) {
3711
2228
  const userAgent = extra?.requestInfo?.headers?.["user-agent"];
3712
2229
  if (userAgent) {
@@ -3714,29 +2231,14 @@ function getClientIdentifier(extra) {
3714
2231
  }
3715
2232
  return "stdio";
3716
2233
  }
2234
+
2235
+ // src/tools/execute-sql.ts
3717
2236
  var executeSqlSchema = {
3718
2237
  sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
3719
2238
  };
3720
2239
  function splitSQLStatements(sql2) {
3721
2240
  return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
3722
2241
  }
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);
3739
- }
3740
2242
  function areAllStatementsReadOnly(sql2, connectorType) {
3741
2243
  const statements = splitSQLStatements(sql2);
3742
2244
  return statements.every((statement) => isReadOnlySQL(statement, connectorType));
@@ -3751,13 +2253,19 @@ function createExecuteSqlToolHandler(sourceId) {
3751
2253
  let result;
3752
2254
  try {
3753
2255
  const connector = ConnectorManager.getCurrentConnector(sourceId);
3754
- const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
3755
- const isReadonly = executeOptions.readonly === true;
2256
+ const actualSourceId = connector.getId();
2257
+ const registry = getToolRegistry();
2258
+ const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
2259
+ const isReadonly = toolConfig?.readonly === true;
3756
2260
  if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
3757
- errorMessage = `Read-only mode is enabled for source '${effectiveSourceId}'. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
2261
+ errorMessage = `Read-only mode is enabled for source '${actualSourceId}'. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
3758
2262
  success = false;
3759
2263
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
3760
2264
  }
2265
+ const executeOptions = {
2266
+ readonly: isReadonly,
2267
+ max_rows: toolConfig?.max_rows
2268
+ };
3761
2269
  result = await connector.executeSQL(sql2, executeOptions);
3762
2270
  const responseData = {
3763
2271
  rows: result.rows,
@@ -4151,11 +2659,11 @@ function zodToParameters(schema) {
4151
2659
  }
4152
2660
  return parameters;
4153
2661
  }
4154
- function getToolMetadataForSource(sourceId) {
2662
+ function getExecuteSqlMetadata(sourceId) {
4155
2663
  const sourceIds = ConnectorManager.getAvailableSourceIds();
4156
2664
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
4157
2665
  const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
4158
- const dbType = sourceConfig?.type || "database";
2666
+ const dbType = sourceConfig.type;
4159
2667
  const toolName = sourceId === "default" ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
4160
2668
  const isDefault = sourceIds[0] === sourceId;
4161
2669
  const title = isDefault ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
@@ -4182,473 +2690,274 @@ function getToolMetadataForSource(sourceId) {
4182
2690
  annotations
4183
2691
  };
4184
2692
  }
4185
- function getToolsForSource(sourceId) {
4186
- const metadata = getToolMetadataForSource(sourceId);
4187
- const parameters = zodToParameters(metadata.schema);
4188
- return [
4189
- {
4190
- name: metadata.name,
4191
- description: metadata.description,
4192
- parameters
4193
- }
4194
- ];
2693
+ function getSearchObjectsMetadata(sourceId, dbType, isDefault) {
2694
+ const toolName = sourceId === "default" ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
2695
+ const title = isDefault ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
2696
+ 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.`;
2697
+ return {
2698
+ name: toolName,
2699
+ description,
2700
+ title
2701
+ };
4195
2702
  }
4196
-
4197
- // src/tools/index.ts
4198
- function registerTools(server) {
4199
- const sourceIds = ConnectorManager.getAvailableSourceIds();
4200
- if (sourceIds.length === 0) {
4201
- throw new Error("No database sources configured");
2703
+ function customParamsToToolParams(params) {
2704
+ if (!params || params.length === 0) {
2705
+ return [];
4202
2706
  }
4203
- for (const sourceId of sourceIds) {
4204
- const isDefault = sourceIds[0] === sourceId;
4205
- const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
4206
- const dbType = sourceConfig?.type || "database";
4207
- const executeSqlMetadata = getToolMetadataForSource(sourceId);
4208
- server.registerTool(
4209
- executeSqlMetadata.name,
2707
+ return params.map((param) => ({
2708
+ name: param.name,
2709
+ type: param.type,
2710
+ required: param.required !== false && param.default === void 0,
2711
+ description: param.description
2712
+ }));
2713
+ }
2714
+ function getToolsForSource(sourceId) {
2715
+ const tools = [];
2716
+ const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
2717
+ const dbType = sourceConfig.type;
2718
+ const sourceIds = ConnectorManager.getAvailableSourceIds();
2719
+ const isDefault = sourceIds[0] === sourceId;
2720
+ const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
2721
+ const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
2722
+ tools.push({
2723
+ name: executeSqlMetadata.name,
2724
+ description: executeSqlMetadata.description,
2725
+ parameters: executeSqlParameters
2726
+ });
2727
+ const searchMetadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
2728
+ tools.push({
2729
+ name: searchMetadata.name,
2730
+ description: searchMetadata.description,
2731
+ parameters: [
4210
2732
  {
4211
- description: executeSqlMetadata.description,
4212
- inputSchema: executeSqlMetadata.schema,
4213
- annotations: executeSqlMetadata.annotations
2733
+ name: "object_type",
2734
+ type: "string",
2735
+ required: true,
2736
+ description: "Type of database object to search for (schema, table, column, procedure, index)"
4214
2737
  },
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
2738
  {
4223
- description: searchToolDescription,
4224
- inputSchema: searchDatabaseObjectsSchema,
4225
- annotations: {
4226
- title: searchToolTitle,
4227
- readOnlyHint: true,
4228
- destructiveHint: false,
4229
- idempotentHint: true,
4230
- // Operation is read-only and idempotent
4231
- openWorldHint: true
4232
- }
2739
+ name: "pattern",
2740
+ type: "string",
2741
+ required: false,
2742
+ description: "Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."
4233
2743
  },
4234
- createSearchDatabaseObjectsToolHandler(sourceId)
4235
- );
2744
+ {
2745
+ name: "schema",
2746
+ type: "string",
2747
+ required: false,
2748
+ description: "Filter results to a specific schema/database"
2749
+ },
2750
+ {
2751
+ name: "detail_level",
2752
+ type: "string",
2753
+ required: false,
2754
+ description: "Level of detail to return: names (minimal), summary (with metadata), full (complete structure). Defaults to 'names'."
2755
+ },
2756
+ {
2757
+ name: "limit",
2758
+ type: "integer",
2759
+ required: false,
2760
+ description: "Maximum number of results to return (default: 100, max: 1000)"
2761
+ }
2762
+ ]
2763
+ });
2764
+ if (customToolRegistry.isInitialized()) {
2765
+ const customTools = customToolRegistry.getTools();
2766
+ const sourceCustomTools = customTools.filter((tool) => tool.source === sourceId);
2767
+ for (const customTool of sourceCustomTools) {
2768
+ tools.push({
2769
+ name: customTool.name,
2770
+ description: customTool.description,
2771
+ parameters: customParamsToToolParams(customTool.parameters)
2772
+ });
2773
+ }
4236
2774
  }
2775
+ return tools;
4237
2776
  }
4238
2777
 
4239
- // src/prompts/sql-generator.ts
2778
+ // src/tools/custom-tool-handler.ts
4240
2779
  import { z as z4 } from "zod";
4241
- var sqlGeneratorSchema = {
4242
- description: z4.string().describe("Natural language description of the SQL query to generate"),
4243
- schema: z4.string().optional().describe("Optional database schema to use")
4244
- };
4245
- async function sqlGeneratorPromptHandler({
4246
- description,
4247
- schema
4248
- }, _extra) {
4249
- try {
4250
- const connector = ConnectorManager.getCurrentConnector();
4251
- let sqlDialect;
4252
- switch (connector.id) {
4253
- case "postgres":
4254
- sqlDialect = "postgres";
2780
+ function buildZodSchemaFromParameters(parameters) {
2781
+ if (!parameters || parameters.length === 0) {
2782
+ return {};
2783
+ }
2784
+ const schemaShape = {};
2785
+ for (const param of parameters) {
2786
+ let fieldSchema;
2787
+ switch (param.type) {
2788
+ case "string":
2789
+ fieldSchema = z4.string().describe(param.description);
2790
+ break;
2791
+ case "integer":
2792
+ fieldSchema = z4.number().int().describe(param.description);
4255
2793
  break;
4256
- case "sqlite":
4257
- sqlDialect = "sqlite";
2794
+ case "float":
2795
+ fieldSchema = z4.number().describe(param.description);
4258
2796
  break;
4259
- case "mysql":
4260
- sqlDialect = "mysql";
2797
+ case "boolean":
2798
+ fieldSchema = z4.boolean().describe(param.description);
4261
2799
  break;
4262
- case "sqlserver":
4263
- sqlDialect = "mssql";
2800
+ case "array":
2801
+ fieldSchema = z4.array(z4.unknown()).describe(param.description);
4264
2802
  break;
4265
2803
  default:
4266
- sqlDialect = "ansi";
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
- }
2804
+ throw new Error(`Unsupported parameter type: ${param.type}`);
4276
2805
  }
4277
- try {
4278
- const tables = await connector.getTables(schema);
4279
- if (tables.length === 0) {
4280
- const schemaInfo2 = schema ? `in schema '${schema}'` : "in the database";
4281
- return formatPromptErrorResponse(
4282
- `No tables found ${schemaInfo2}. Please check your database connection or schema name.`,
4283
- "NO_TABLES_FOUND"
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;
2806
+ if (param.allowed_values && param.allowed_values.length > 0) {
2807
+ if (param.type === "string") {
2808
+ fieldSchema = z4.enum(param.allowed_values).describe(param.description);
2809
+ } else {
2810
+ fieldSchema = fieldSchema.refine(
2811
+ (val) => param.allowed_values.includes(val),
2812
+ {
2813
+ message: `Value must be one of: ${param.allowed_values.join(", ")}`
4299
2814
  }
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
2815
  );
4308
2816
  }
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
2817
  }
4401
- } catch (error) {
4402
- return formatPromptErrorResponse(
4403
- `Failed to generate SQL: ${error.message}`,
4404
- "SQL_GENERATION_ERROR"
4405
- );
2818
+ if (param.default !== void 0 || param.required === false) {
2819
+ fieldSchema = fieldSchema.optional();
2820
+ }
2821
+ schemaShape[param.name] = fieldSchema;
4406
2822
  }
2823
+ return schemaShape;
4407
2824
  }
4408
-
4409
- // src/prompts/db-explainer.ts
4410
- import { z as z5 } from "zod";
4411
- var dbExplainerSchema = {
4412
- schema: z5.string().optional().describe("Optional database schema to use"),
4413
- table: z5.string().optional().describe("Optional specific table to explain")
4414
- };
4415
- async function dbExplainerPromptHandler({
4416
- schema,
4417
- table
4418
- }, _extra) {
4419
- try {
4420
- const connector = ConnectorManager.getCurrentConnector();
4421
- if (schema) {
4422
- const availableSchemas = await connector.getSchemas();
4423
- if (!availableSchemas.includes(schema)) {
4424
- return formatPromptErrorResponse(
4425
- `Schema '${schema}' does not exist or cannot be accessed. Available schemas: ${availableSchemas.join(", ")}`,
4426
- "SCHEMA_NOT_FOUND"
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
- );
2825
+ function createCustomToolHandler(toolConfig) {
2826
+ const zodSchemaShape = buildZodSchemaFromParameters(toolConfig.parameters);
2827
+ const zodSchema = z4.object(zodSchemaShape);
2828
+ return async (args, extra) => {
2829
+ const startTime = Date.now();
2830
+ let success = true;
2831
+ let errorMessage;
2832
+ let paramValues = [];
2833
+ try {
2834
+ const validatedArgs = zodSchema.parse(args);
2835
+ const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
2836
+ const executeOptions = ConnectorManager.getCurrentExecuteOptions(
2837
+ toolConfig.source
2838
+ );
2839
+ const isReadonly = executeOptions.readonly === true;
2840
+ if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
2841
+ 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"}`;
2842
+ success = false;
2843
+ return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
4469
2844
  }
4470
- try {
4471
- const columns = await connector.getTableSchema(tableName, schema);
4472
- const column = columns.find(
4473
- (c) => c.column_name.toLowerCase() === columnName.toLowerCase()
4474
- );
4475
- if (column) {
4476
- const columnDescription = `Column: ${tableName}.${column.column_name}
4477
-
4478
- Type: ${column.data_type}
4479
- Nullable: ${column.is_nullable === "YES" ? "Yes" : "No"}
4480
- Default: ${column.column_default || "None"}
2845
+ paramValues = mapArgumentsToArray(
2846
+ toolConfig.parameters,
2847
+ validatedArgs
2848
+ );
2849
+ const result = await connector.executeSQL(
2850
+ toolConfig.statement,
2851
+ executeOptions,
2852
+ paramValues
2853
+ );
2854
+ const responseData = {
2855
+ rows: result.rows,
2856
+ count: result.rows.length,
2857
+ source_id: toolConfig.source
2858
+ };
2859
+ return createToolSuccessResponse(responseData);
2860
+ } catch (error) {
2861
+ success = false;
2862
+ errorMessage = error.message;
2863
+ if (error instanceof z4.ZodError) {
2864
+ const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2865
+ errorMessage = `Parameter validation failed: ${issues}`;
2866
+ } else {
2867
+ errorMessage = `${errorMessage}
4481
2868
 
4482
- Purpose:
4483
- ${determineColumnPurpose(column.column_name, column.data_type)}`;
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
- );
2869
+ SQL: ${toolConfig.statement}
2870
+ Parameters: ${JSON.stringify(paramValues)}`;
4496
2871
  }
2872
+ return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
2873
+ } finally {
2874
+ requestStore.add({
2875
+ id: crypto.randomUUID(),
2876
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2877
+ sourceId: toolConfig.source,
2878
+ toolName: toolConfig.name,
2879
+ sql: toolConfig.statement,
2880
+ durationMs: Date.now() - startTime,
2881
+ client: getClientIdentifier(extra),
2882
+ success,
2883
+ error: errorMessage
2884
+ });
4497
2885
  }
4498
- if (!table || ["database", "db", "schema", "overview", "all"].includes(normalizedTable)) {
4499
- const schemaInfo = schema ? `in schema '${schema}'` : "across all schemas";
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?
2886
+ };
2887
+ }
4515
2888
 
4516
- ${possibleTableMatches.join("\n")}`
4517
- );
2889
+ // src/tools/index.ts
2890
+ function registerTools(server) {
2891
+ const sourceIds = ConnectorManager.getAvailableSourceIds();
2892
+ if (sourceIds.length === 0) {
2893
+ throw new Error("No database sources configured");
2894
+ }
2895
+ const registry = getToolRegistry();
2896
+ for (const sourceId of sourceIds) {
2897
+ const enabledTools = registry.getToolsForSource(sourceId);
2898
+ const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
2899
+ const dbType = sourceConfig.type;
2900
+ const isDefault = sourceIds[0] === sourceId;
2901
+ for (const toolConfig of enabledTools) {
2902
+ if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
2903
+ registerExecuteSqlTool(server, sourceId, dbType);
2904
+ } else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
2905
+ registerSearchObjectsTool(server, sourceId, dbType, isDefault);
4518
2906
  } else {
4519
- const schemaInfo = schema ? `in schema '${schema}'` : "in the database";
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
- );
2907
+ registerCustomTool(server, toolConfig, dbType);
4524
2908
  }
4525
2909
  }
4526
- } catch (error) {
4527
- return formatPromptErrorResponse(
4528
- `Error explaining database: ${error.message}`,
4529
- "EXPLANATION_ERROR"
4530
- );
4531
2910
  }
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
- }
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
2911
  }
4557
- function determineRelationships(tableName, columns) {
4558
- const potentialRelationships = [];
4559
- const idColumns = columns.filter(
4560
- (c) => c.column_name.toLowerCase().endsWith("_id") && !c.column_name.toLowerCase().startsWith(tableName.toLowerCase())
2912
+ function registerExecuteSqlTool(server, sourceId, dbType) {
2913
+ const metadata = getExecuteSqlMetadata(sourceId);
2914
+ server.registerTool(
2915
+ metadata.name,
2916
+ {
2917
+ description: metadata.description,
2918
+ inputSchema: metadata.schema,
2919
+ annotations: metadata.annotations
2920
+ },
2921
+ createExecuteSqlToolHandler(sourceId)
4561
2922
  );
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
- }
4618
- function describeDatabasePurpose(tables) {
4619
- const tableNames = tables.map((t) => t.toLowerCase());
4620
- if (tableNames.some((t) => t.includes("user")) && tableNames.some((t) => t.includes("order"))) {
4621
- return "appears to be an e-commerce or customer order management system";
4622
- }
4623
- if (tableNames.some((t) => t.includes("patient")) || tableNames.some((t) => t.includes("medical"))) {
4624
- return "appears to be related to healthcare or medical record management";
4625
- }
4626
- if (tableNames.some((t) => t.includes("student")) || tableNames.some((t) => t.includes("course"))) {
4627
- return "appears to be related to education or student management";
4628
- }
4629
- if (tableNames.some((t) => t.includes("employee")) || tableNames.some((t) => t.includes("payroll"))) {
4630
- return "appears to be related to HR or employee management";
4631
- }
4632
- if (tableNames.some((t) => t.includes("inventory")) || tableNames.some((t) => t.includes("stock"))) {
4633
- return "appears to be related to inventory or stock management";
4634
- }
4635
- return "contains multiple tables that store related information";
4636
2923
  }
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
2924
+ function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
2925
+ const metadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
2926
+ server.registerTool(
2927
+ metadata.name,
2928
+ {
2929
+ description: metadata.description,
2930
+ inputSchema: searchDatabaseObjectsSchema,
2931
+ annotations: {
2932
+ title: metadata.title,
2933
+ readOnlyHint: true,
2934
+ destructiveHint: false,
2935
+ idempotentHint: true,
2936
+ openWorldHint: true
2937
+ }
2938
+ },
2939
+ createSearchDatabaseObjectsToolHandler(sourceId)
4645
2940
  );
4646
- server.prompt(
4647
- "explain_db",
4648
- "Get explanations about database tables, columns, and structures",
4649
- dbExplainerSchema,
4650
- dbExplainerPromptHandler
2941
+ }
2942
+ function registerCustomTool(server, toolConfig, dbType) {
2943
+ const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
2944
+ const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
2945
+ server.registerTool(
2946
+ toolConfig.name,
2947
+ {
2948
+ description: toolConfig.description,
2949
+ inputSchema: zodSchema,
2950
+ annotations: {
2951
+ title: `${toolConfig.name} (${dbType})`,
2952
+ readOnlyHint: isReadOnly,
2953
+ destructiveHint: !isReadOnly,
2954
+ idempotentHint: isReadOnly,
2955
+ openWorldHint: false
2956
+ }
2957
+ },
2958
+ createCustomToolHandler(toolConfig)
4651
2959
  );
2960
+ console.error(` - ${toolConfig.name} \u2192 ${toolConfig.source} (${dbType})`);
4652
2961
  }
4653
2962
 
4654
2963
  // src/api/sources.ts
@@ -4759,11 +3068,127 @@ function listRequests(req, res) {
4759
3068
  }
4760
3069
  }
4761
3070
 
3071
+ // src/utils/startup-table.ts
3072
+ var BOX = {
3073
+ topLeft: "\u250C",
3074
+ topRight: "\u2510",
3075
+ bottomLeft: "\u2514",
3076
+ bottomRight: "\u2518",
3077
+ horizontal: "\u2500",
3078
+ vertical: "\u2502",
3079
+ leftT: "\u251C",
3080
+ rightT: "\u2524",
3081
+ bullet: "\u2022"
3082
+ };
3083
+ function parseHostAndDatabase(source) {
3084
+ if (source.dsn) {
3085
+ const parsed = parseConnectionInfoFromDSN(source.dsn);
3086
+ if (parsed) {
3087
+ if (parsed.type === "sqlite") {
3088
+ return { host: "", database: parsed.database || ":memory:" };
3089
+ }
3090
+ if (!parsed.host) {
3091
+ return { host: "", database: parsed.database || "" };
3092
+ }
3093
+ const port = parsed.port ?? getDefaultPortForType(parsed.type);
3094
+ const host2 = port ? `${parsed.host}:${port}` : parsed.host;
3095
+ return { host: host2, database: parsed.database || "" };
3096
+ }
3097
+ return { host: "unknown", database: "" };
3098
+ }
3099
+ const host = source.host ? source.port ? `${source.host}:${source.port}` : source.host : "";
3100
+ const database = source.database || "";
3101
+ return { host, database };
3102
+ }
3103
+ function horizontalLine(width, left, right) {
3104
+ return left + BOX.horizontal.repeat(width - 2) + right;
3105
+ }
3106
+ function fitString(str, width) {
3107
+ if (str.length > width) {
3108
+ return str.slice(0, width - 1) + "\u2026";
3109
+ }
3110
+ return str.padEnd(width);
3111
+ }
3112
+ function formatHostDatabase(host, database) {
3113
+ return host ? database ? `${host}/${database}` : host : database || "";
3114
+ }
3115
+ function generateStartupTable(sources) {
3116
+ if (sources.length === 0) {
3117
+ return "";
3118
+ }
3119
+ const idTypeWidth = Math.max(
3120
+ 20,
3121
+ ...sources.map((s) => `${s.id} (${s.type})`.length)
3122
+ );
3123
+ const hostDbWidth = Math.max(
3124
+ 24,
3125
+ ...sources.map((s) => formatHostDatabase(s.host, s.database).length)
3126
+ );
3127
+ const modeWidth = Math.max(
3128
+ 10,
3129
+ ...sources.map((s) => {
3130
+ const modes = [];
3131
+ if (s.isDemo) modes.push("DEMO");
3132
+ if (s.readonly) modes.push("READ-ONLY");
3133
+ return modes.join(" ").length;
3134
+ })
3135
+ );
3136
+ const totalWidth = 2 + idTypeWidth + 3 + hostDbWidth + 3 + modeWidth + 2;
3137
+ const lines = [];
3138
+ for (let i = 0; i < sources.length; i++) {
3139
+ const source = sources[i];
3140
+ const isFirst = i === 0;
3141
+ const isLast = i === sources.length - 1;
3142
+ if (isFirst) {
3143
+ lines.push(horizontalLine(totalWidth, BOX.topLeft, BOX.topRight));
3144
+ }
3145
+ const idType = fitString(`${source.id} (${source.type})`, idTypeWidth);
3146
+ const hostDb = fitString(
3147
+ formatHostDatabase(source.host, source.database),
3148
+ hostDbWidth
3149
+ );
3150
+ const modes = [];
3151
+ if (source.isDemo) modes.push("DEMO");
3152
+ if (source.readonly) modes.push("READ-ONLY");
3153
+ const modeStr = fitString(modes.join(" "), modeWidth);
3154
+ lines.push(
3155
+ `${BOX.vertical} ${idType} ${BOX.vertical} ${hostDb} ${BOX.vertical} ${modeStr} ${BOX.vertical}`
3156
+ );
3157
+ lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
3158
+ for (const tool of source.tools) {
3159
+ const toolLine = ` ${BOX.bullet} ${tool}`;
3160
+ lines.push(
3161
+ `${BOX.vertical} ${fitString(toolLine, totalWidth - 4)} ${BOX.vertical}`
3162
+ );
3163
+ }
3164
+ if (isLast) {
3165
+ lines.push(horizontalLine(totalWidth, BOX.bottomLeft, BOX.bottomRight));
3166
+ } else {
3167
+ lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
3168
+ }
3169
+ }
3170
+ return lines.join("\n");
3171
+ }
3172
+ function buildSourceDisplayInfo(sourceConfigs, getToolsForSource2, isDemo) {
3173
+ return sourceConfigs.map((source) => {
3174
+ const { host, database } = parseHostAndDatabase(source);
3175
+ return {
3176
+ id: source.id,
3177
+ type: source.type || "sqlite",
3178
+ host,
3179
+ database,
3180
+ readonly: source.readonly || false,
3181
+ isDemo,
3182
+ tools: getToolsForSource2(source.id)
3183
+ };
3184
+ });
3185
+ }
3186
+
4762
3187
  // src/server.ts
4763
- var __filename2 = fileURLToPath2(import.meta.url);
4764
- var __dirname2 = path3.dirname(__filename2);
4765
- var packageJsonPath = path3.join(__dirname2, "..", "package.json");
4766
- var packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
3188
+ var __filename = fileURLToPath(import.meta.url);
3189
+ var __dirname = path.dirname(__filename);
3190
+ var packageJsonPath = path.join(__dirname, "..", "package.json");
3191
+ var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
4767
3192
  var SERVER_NAME = "DBHub MCP Server";
4768
3193
  var SERVER_VERSION = packageJson.version;
4769
3194
  function generateBanner(version, modes = []) {
@@ -4807,16 +3232,6 @@ See documentation for more details on configuring database connections.
4807
3232
  `);
4808
3233
  process.exit(1);
4809
3234
  }
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
3235
  const connectorManager = new ConnectorManager();
4821
3236
  const sources = sourceConfigsData.sources;
4822
3237
  console.error(`Configuration source: ${sourceConfigsData.source}`);
@@ -4826,13 +3241,35 @@ See documentation for more details on configuring database connections.
4826
3241
  console.error(` - ${source.id}: ${redactDSN(dsn)}`);
4827
3242
  }
4828
3243
  await connectorManager.connectWithSources(sources);
4829
- const transportData = resolveTransport();
4830
- console.error(`MCP transport: ${transportData.type}`);
4831
- const portData = transportData.type === "http" ? resolvePort() : null;
4832
- if (portData) {
4833
- console.error(`HTTP server port: ${portData.port} (source: ${portData.source})`);
3244
+ const { initializeToolRegistry } = await import("./registry-GJGPWR3I.js");
3245
+ initializeToolRegistry({
3246
+ sources: sourceConfigsData.sources,
3247
+ tools: sourceConfigsData.tools
3248
+ });
3249
+ console.error("Tool registry initialized");
3250
+ if (sourceConfigsData.tools && sourceConfigsData.tools.length > 0) {
3251
+ const { customToolRegistry: customToolRegistry2 } = await import("./custom-tool-registry-EW3KOBGC.js");
3252
+ const { BUILTIN_TOOLS } = await import("./builtin-tools-SVENYBIA.js");
3253
+ if (!customToolRegistry2.isInitialized()) {
3254
+ const customTools = sourceConfigsData.tools.filter(
3255
+ (tool) => !BUILTIN_TOOLS.includes(tool.name)
3256
+ );
3257
+ if (customTools.length > 0) {
3258
+ customToolRegistry2.initialize(customTools);
3259
+ console.error(`Custom tool registry initialized with ${customTools.length} tool(s)`);
3260
+ }
3261
+ }
4834
3262
  }
4835
- const readonly = isReadOnlyMode();
3263
+ const createServer = () => {
3264
+ const server = new McpServer({
3265
+ name: SERVER_NAME,
3266
+ version: SERVER_VERSION
3267
+ });
3268
+ registerTools(server);
3269
+ return server;
3270
+ };
3271
+ const transportData = resolveTransport();
3272
+ const port = transportData.type === "http" ? resolvePort().port : null;
4836
3273
  const activeModes = [];
4837
3274
  const modeDescriptions = [];
4838
3275
  const isDemo = isDemoMode();
@@ -4840,19 +3277,17 @@ See documentation for more details on configuring database connections.
4840
3277
  activeModes.push("DEMO");
4841
3278
  modeDescriptions.push("using sample employee database");
4842
3279
  }
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
3280
  if (activeModes.length > 0) {
4851
3281
  console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
4852
3282
  }
4853
3283
  console.error(generateBanner(SERVER_VERSION, activeModes));
3284
+ const sourceDisplayInfos = buildSourceDisplayInfo(
3285
+ sources,
3286
+ (sourceId) => getToolsForSource(sourceId).map((t) => t.name),
3287
+ isDemo
3288
+ );
3289
+ console.error(generateStartupTable(sourceDisplayInfos));
4854
3290
  if (transportData.type === "http") {
4855
- const port = portData.port;
4856
3291
  const app = express();
4857
3292
  app.use(express.json());
4858
3293
  app.use((req, res, next) => {
@@ -4869,7 +3304,7 @@ See documentation for more details on configuring database connections.
4869
3304
  }
4870
3305
  next();
4871
3306
  });
4872
- const frontendPath = path3.join(__dirname2, "public");
3307
+ const frontendPath = path.join(__dirname, "public");
4873
3308
  app.use(express.static(frontendPath));
4874
3309
  app.get("/healthz", (req, res) => {
4875
3310
  res.status(200).send("OK");
@@ -4891,7 +3326,7 @@ See documentation for more details on configuring database connections.
4891
3326
  enableJsonResponse: true
4892
3327
  // Use JSON responses (SSE not supported in stateless mode)
4893
3328
  });
4894
- const server = createServer2();
3329
+ const server = createServer();
4895
3330
  await server.connect(transport);
4896
3331
  await transport.handleRequest(req, res, req.body);
4897
3332
  } catch (error) {
@@ -4903,7 +3338,7 @@ See documentation for more details on configuring database connections.
4903
3338
  });
4904
3339
  if (process.env.NODE_ENV !== "development") {
4905
3340
  app.get("*", (req, res) => {
4906
- res.sendFile(path3.join(frontendPath, "index.html"));
3341
+ res.sendFile(path.join(frontendPath, "index.html"));
4907
3342
  });
4908
3343
  }
4909
3344
  app.listen(port, "0.0.0.0", () => {
@@ -4918,7 +3353,7 @@ See documentation for more details on configuring database connections.
4918
3353
  console.error(`MCP server endpoint at http://0.0.0.0:${port}/mcp`);
4919
3354
  });
4920
3355
  } else {
4921
- const server = createServer2();
3356
+ const server = createServer();
4922
3357
  const transport = new StdioServerTransport();
4923
3358
  await server.connect(transport);
4924
3359
  console.error("MCP server running on stdio");