@bytebase/dbhub 0.13.2 → 0.15.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
@@ -5,7 +5,6 @@ import {
5
5
  ConnectorManager,
6
6
  ConnectorRegistry,
7
7
  SafeURL,
8
- buildDSNFromSource,
9
8
  getDatabaseTypeFromDSN,
10
9
  getDefaultPortForType,
11
10
  getToolRegistry,
@@ -13,12 +12,11 @@ import {
13
12
  mapArgumentsToArray,
14
13
  obfuscateDSNPassword,
15
14
  parseConnectionInfoFromDSN,
16
- redactDSN,
17
15
  resolvePort,
18
16
  resolveSourceConfigs,
19
17
  resolveTransport,
20
18
  stripCommentsAndStrings
21
- } from "./chunk-KBVJEDZF.js";
19
+ } from "./chunk-TPHNNFR5.js";
22
20
 
23
21
  // src/connectors/postgres/index.ts
24
22
  import pg from "pg";
@@ -148,6 +146,7 @@ var { Pool } = pg;
148
146
  var PostgresDSNParser = class {
149
147
  async parse(dsn, config) {
150
148
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
149
+ const queryTimeoutSeconds = config?.queryTimeoutSeconds;
151
150
  if (!this.isValidDSN(dsn)) {
152
151
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
153
152
  const expectedFormat = this.getSampleDSN();
@@ -159,7 +158,7 @@ Expected: ${expectedFormat}`
159
158
  }
160
159
  try {
161
160
  const url = new SafeURL(dsn);
162
- const config2 = {
161
+ const poolConfig = {
163
162
  host: url.hostname,
164
163
  port: url.port ? parseInt(url.port) : 5432,
165
164
  database: url.pathname ? url.pathname.substring(1) : "",
@@ -170,18 +169,21 @@ Expected: ${expectedFormat}`
170
169
  url.forEachSearchParam((value, key) => {
171
170
  if (key === "sslmode") {
172
171
  if (value === "disable") {
173
- config2.ssl = false;
172
+ poolConfig.ssl = false;
174
173
  } else if (value === "require") {
175
- config2.ssl = { rejectUnauthorized: false };
174
+ poolConfig.ssl = { rejectUnauthorized: false };
176
175
  } else {
177
- config2.ssl = true;
176
+ poolConfig.ssl = true;
178
177
  }
179
178
  }
180
179
  });
181
180
  if (connectionTimeoutSeconds !== void 0) {
182
- config2.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
181
+ poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
183
182
  }
184
- return config2;
183
+ if (queryTimeoutSeconds !== void 0) {
184
+ poolConfig.query_timeout = queryTimeoutSeconds * 1e3;
185
+ }
186
+ return poolConfig;
185
187
  } catch (error) {
186
188
  throw new Error(
187
189
  `Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`
@@ -475,22 +477,26 @@ var PostgresConnector = class _PostgresConnector {
475
477
  const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
476
478
  if (statements.length === 1) {
477
479
  const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
480
+ let result;
478
481
  if (parameters && parameters.length > 0) {
479
482
  try {
480
- return await client.query(processedStatement, parameters);
483
+ result = await client.query(processedStatement, parameters);
481
484
  } catch (error) {
482
485
  console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
483
486
  console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
484
487
  console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
485
488
  throw error;
486
489
  }
490
+ } else {
491
+ result = await client.query(processedStatement);
487
492
  }
488
- return await client.query(processedStatement);
493
+ return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length };
489
494
  } else {
490
495
  if (parameters && parameters.length > 0) {
491
496
  throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
492
497
  }
493
498
  let allRows = [];
499
+ let totalRowCount = 0;
494
500
  await client.query("BEGIN");
495
501
  try {
496
502
  for (let statement of statements) {
@@ -499,13 +505,16 @@ var PostgresConnector = class _PostgresConnector {
499
505
  if (result.rows && result.rows.length > 0) {
500
506
  allRows.push(...result.rows);
501
507
  }
508
+ if (result.rowCount) {
509
+ totalRowCount += result.rowCount;
510
+ }
502
511
  }
503
512
  await client.query("COMMIT");
504
513
  } catch (error) {
505
514
  await client.query("ROLLBACK");
506
515
  throw error;
507
516
  }
508
- return { rows: allRows };
517
+ return { rows: allRows, rowCount: totalRowCount };
509
518
  }
510
519
  } finally {
511
520
  client.release();
@@ -521,7 +530,7 @@ import { DefaultAzureCredential } from "@azure/identity";
521
530
  var SQLServerDSNParser = class {
522
531
  async parse(dsn, config) {
523
532
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
524
- const requestTimeoutSeconds = config?.requestTimeoutSeconds;
533
+ const queryTimeoutSeconds = config?.queryTimeoutSeconds;
525
534
  if (!this.isValidDSN(dsn)) {
526
535
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
527
536
  const expectedFormat = this.getSampleDSN();
@@ -541,8 +550,16 @@ Expected: ${expectedFormat}`
541
550
  options.sslmode = value;
542
551
  } else if (key === "instanceName") {
543
552
  options.instanceName = value;
553
+ } else if (key === "domain") {
554
+ options.domain = value;
544
555
  }
545
556
  });
557
+ if (options.authentication === "ntlm" && !options.domain) {
558
+ throw new Error("NTLM authentication requires 'domain' parameter");
559
+ }
560
+ if (options.domain && options.authentication !== "ntlm") {
561
+ throw new Error("Parameter 'domain' requires 'authentication=ntlm'");
562
+ }
546
563
  if (options.sslmode) {
547
564
  if (options.sslmode === "disable") {
548
565
  options.encrypt = false;
@@ -553,8 +570,6 @@ Expected: ${expectedFormat}`
553
570
  }
554
571
  }
555
572
  const config2 = {
556
- user: url.username,
557
- password: url.password,
558
573
  server: url.hostname,
559
574
  port: url.port ? parseInt(url.port) : 1433,
560
575
  // Default SQL Server port
@@ -567,27 +582,44 @@ Expected: ${expectedFormat}`
567
582
  ...connectionTimeoutSeconds !== void 0 && {
568
583
  connectTimeout: connectionTimeoutSeconds * 1e3
569
584
  },
570
- ...requestTimeoutSeconds !== void 0 && {
571
- requestTimeout: requestTimeoutSeconds * 1e3
585
+ ...queryTimeoutSeconds !== void 0 && {
586
+ requestTimeout: queryTimeoutSeconds * 1e3
572
587
  },
573
588
  instanceName: options.instanceName
574
589
  // Add named instance support
575
590
  }
576
591
  };
577
- if (options.authentication === "azure-active-directory-access-token") {
578
- try {
579
- const credential = new DefaultAzureCredential();
580
- const token = await credential.getToken("https://database.windows.net/");
592
+ switch (options.authentication) {
593
+ case "azure-active-directory-access-token": {
594
+ try {
595
+ const credential = new DefaultAzureCredential();
596
+ const token = await credential.getToken("https://database.windows.net/");
597
+ config2.authentication = {
598
+ type: "azure-active-directory-access-token",
599
+ options: {
600
+ token: token.token
601
+ }
602
+ };
603
+ } catch (error) {
604
+ const errorMessage = error instanceof Error ? error.message : String(error);
605
+ throw new Error(`Failed to get Azure AD token: ${errorMessage}`);
606
+ }
607
+ break;
608
+ }
609
+ case "ntlm":
581
610
  config2.authentication = {
582
- type: "azure-active-directory-access-token",
611
+ type: "ntlm",
583
612
  options: {
584
- token: token.token
613
+ domain: options.domain,
614
+ userName: url.username,
615
+ password: url.password
585
616
  }
586
617
  };
587
- } catch (error) {
588
- const errorMessage = error instanceof Error ? error.message : String(error);
589
- throw new Error(`Failed to get Azure AD token: ${errorMessage}`);
590
- }
618
+ break;
619
+ default:
620
+ config2.user = url.username;
621
+ config2.password = url.password;
622
+ break;
591
623
  }
592
624
  return config2;
593
625
  } catch (error) {
@@ -904,9 +936,6 @@ var SQLServerConnector = class _SQLServerConnector {
904
936
  }
905
937
  return {
906
938
  rows: result.recordset || [],
907
- fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
908
- name: key
909
- })) : [],
910
939
  rowCount: result.rowsAffected[0] || 0
911
940
  };
912
941
  } catch (error) {
@@ -1190,7 +1219,7 @@ var SQLiteConnector = class _SQLiteConnector {
1190
1219
  if (parameters && parameters.length > 0) {
1191
1220
  try {
1192
1221
  const rows = this.db.prepare(processedStatement).all(...parameters);
1193
- return { rows };
1222
+ return { rows, rowCount: rows.length };
1194
1223
  } catch (error) {
1195
1224
  console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
1196
1225
  console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
@@ -1199,12 +1228,13 @@ var SQLiteConnector = class _SQLiteConnector {
1199
1228
  }
1200
1229
  } else {
1201
1230
  const rows = this.db.prepare(processedStatement).all();
1202
- return { rows };
1231
+ return { rows, rowCount: rows.length };
1203
1232
  }
1204
1233
  } else {
1234
+ let result;
1205
1235
  if (parameters && parameters.length > 0) {
1206
1236
  try {
1207
- this.db.prepare(processedStatement).run(...parameters);
1237
+ result = this.db.prepare(processedStatement).run(...parameters);
1208
1238
  } catch (error) {
1209
1239
  console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
1210
1240
  console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
@@ -1212,9 +1242,9 @@ var SQLiteConnector = class _SQLiteConnector {
1212
1242
  throw error;
1213
1243
  }
1214
1244
  } else {
1215
- this.db.prepare(processedStatement).run();
1245
+ result = this.db.prepare(processedStatement).run();
1216
1246
  }
1217
- return { rows: [] };
1247
+ return { rows: [], rowCount: result.changes };
1218
1248
  }
1219
1249
  } else {
1220
1250
  if (parameters && parameters.length > 0) {
@@ -1230,8 +1260,10 @@ var SQLiteConnector = class _SQLiteConnector {
1230
1260
  writeStatements.push(statement);
1231
1261
  }
1232
1262
  }
1233
- if (writeStatements.length > 0) {
1234
- this.db.exec(writeStatements.join("; "));
1263
+ let totalChanges = 0;
1264
+ for (const statement of writeStatements) {
1265
+ const result = this.db.prepare(statement).run();
1266
+ totalChanges += result.changes;
1235
1267
  }
1236
1268
  let allRows = [];
1237
1269
  for (let statement of readStatements) {
@@ -1239,7 +1271,7 @@ var SQLiteConnector = class _SQLiteConnector {
1239
1271
  const result = this.db.prepare(statement).all();
1240
1272
  allRows.push(...result);
1241
1273
  }
1242
- return { rows: allRows };
1274
+ return { rows: allRows, rowCount: totalChanges + allRows.length };
1243
1275
  }
1244
1276
  } catch (error) {
1245
1277
  throw error;
@@ -1278,6 +1310,26 @@ function extractRowsFromMultiStatement(results) {
1278
1310
  }
1279
1311
  return allRows;
1280
1312
  }
1313
+ function extractAffectedRows(results) {
1314
+ if (isMetadataObject(results)) {
1315
+ return results.affectedRows || 0;
1316
+ }
1317
+ if (!Array.isArray(results)) {
1318
+ return 0;
1319
+ }
1320
+ if (isMultiStatementResult(results)) {
1321
+ let totalAffected = 0;
1322
+ for (const result of results) {
1323
+ if (isMetadataObject(result)) {
1324
+ totalAffected += result.affectedRows || 0;
1325
+ } else if (Array.isArray(result)) {
1326
+ totalAffected += result.length;
1327
+ }
1328
+ }
1329
+ return totalAffected;
1330
+ }
1331
+ return results.length;
1332
+ }
1281
1333
  function parseQueryResults(results) {
1282
1334
  if (!Array.isArray(results)) {
1283
1335
  return [];
@@ -1374,6 +1426,9 @@ var MySQLConnector = class _MySQLConnector {
1374
1426
  try {
1375
1427
  const connectionOptions = await this.dsnParser.parse(dsn, config);
1376
1428
  this.pool = mysql.createPool(connectionOptions);
1429
+ if (config?.queryTimeoutSeconds !== void 0) {
1430
+ this.queryTimeoutMs = config.queryTimeoutSeconds * 1e3;
1431
+ }
1377
1432
  const [rows] = await this.pool.query("SELECT 1");
1378
1433
  } catch (err) {
1379
1434
  console.error("Failed to connect to MySQL database:", err);
@@ -1676,7 +1731,7 @@ var MySQLConnector = class _MySQLConnector {
1676
1731
  let results;
1677
1732
  if (parameters && parameters.length > 0) {
1678
1733
  try {
1679
- results = await conn.query(processedSQL, parameters);
1734
+ results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs }, parameters);
1680
1735
  } catch (error) {
1681
1736
  console.error(`[MySQL executeSQL] ERROR: ${error.message}`);
1682
1737
  console.error(`[MySQL executeSQL] SQL: ${processedSQL}`);
@@ -1684,11 +1739,12 @@ var MySQLConnector = class _MySQLConnector {
1684
1739
  throw error;
1685
1740
  }
1686
1741
  } else {
1687
- results = await conn.query(processedSQL);
1742
+ results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs });
1688
1743
  }
1689
1744
  const [firstResult] = results;
1690
1745
  const rows = parseQueryResults(firstResult);
1691
- return { rows };
1746
+ const rowCount = extractAffectedRows(firstResult);
1747
+ return { rows, rowCount };
1692
1748
  } catch (error) {
1693
1749
  console.error("Error executing query:", error);
1694
1750
  throw error;
@@ -1705,6 +1761,7 @@ import mariadb from "mariadb";
1705
1761
  var MariadbDSNParser = class {
1706
1762
  async parse(dsn, config) {
1707
1763
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
1764
+ const queryTimeoutSeconds = config?.queryTimeoutSeconds;
1708
1765
  if (!this.isValidDSN(dsn)) {
1709
1766
  const obfuscatedDSN = obfuscateDSNPassword(dsn);
1710
1767
  const expectedFormat = this.getSampleDSN();
@@ -1716,7 +1773,7 @@ Expected: ${expectedFormat}`
1716
1773
  }
1717
1774
  try {
1718
1775
  const url = new SafeURL(dsn);
1719
- const config2 = {
1776
+ const connectionConfig = {
1720
1777
  host: url.hostname,
1721
1778
  port: url.port ? parseInt(url.port) : 3306,
1722
1779
  database: url.pathname ? url.pathname.substring(1) : "",
@@ -1727,25 +1784,28 @@ Expected: ${expectedFormat}`
1727
1784
  // Enable native multi-statement support
1728
1785
  ...connectionTimeoutSeconds !== void 0 && {
1729
1786
  connectTimeout: connectionTimeoutSeconds * 1e3
1787
+ },
1788
+ ...queryTimeoutSeconds !== void 0 && {
1789
+ queryTimeout: queryTimeoutSeconds * 1e3
1730
1790
  }
1731
1791
  };
1732
1792
  url.forEachSearchParam((value, key) => {
1733
1793
  if (key === "sslmode") {
1734
1794
  if (value === "disable") {
1735
- config2.ssl = void 0;
1795
+ connectionConfig.ssl = void 0;
1736
1796
  } else if (value === "require") {
1737
- config2.ssl = { rejectUnauthorized: false };
1797
+ connectionConfig.ssl = { rejectUnauthorized: false };
1738
1798
  } else {
1739
- config2.ssl = {};
1799
+ connectionConfig.ssl = {};
1740
1800
  }
1741
1801
  }
1742
1802
  });
1743
1803
  if (url.password && url.password.includes("X-Amz-Credential")) {
1744
- if (config2.ssl === void 0) {
1745
- config2.ssl = { rejectUnauthorized: false };
1804
+ if (connectionConfig.ssl === void 0) {
1805
+ connectionConfig.ssl = { rejectUnauthorized: false };
1746
1806
  }
1747
1807
  }
1748
- return config2;
1808
+ return connectionConfig;
1749
1809
  } catch (error) {
1750
1810
  throw new Error(
1751
1811
  `Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
@@ -2095,7 +2155,8 @@ var MariaDBConnector = class _MariaDBConnector {
2095
2155
  results = await conn.query(processedSQL);
2096
2156
  }
2097
2157
  const rows = parseQueryResults(results);
2098
- return { rows };
2158
+ const rowCount = extractAffectedRows(results);
2159
+ return { rows, rowCount };
2099
2160
  } catch (error) {
2100
2161
  console.error("Error executing query:", error);
2101
2162
  throw error;
@@ -2242,9 +2303,30 @@ function getClientIdentifier(extra) {
2242
2303
  return "stdio";
2243
2304
  }
2244
2305
 
2306
+ // src/utils/tool-handler-helpers.ts
2307
+ function getEffectiveSourceId(sourceId) {
2308
+ return sourceId || "default";
2309
+ }
2310
+ function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
2311
+ return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
2312
+ }
2313
+ function trackToolRequest(metadata, startTime, extra, success, error) {
2314
+ requestStore.add({
2315
+ id: crypto.randomUUID(),
2316
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2317
+ sourceId: metadata.sourceId,
2318
+ toolName: metadata.toolName,
2319
+ sql: metadata.sql,
2320
+ durationMs: Date.now() - startTime,
2321
+ client: getClientIdentifier(extra),
2322
+ success,
2323
+ error
2324
+ });
2325
+ }
2326
+
2245
2327
  // src/tools/execute-sql.ts
2246
2328
  var executeSqlSchema = {
2247
- sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
2329
+ sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
2248
2330
  };
2249
2331
  function splitSQLStatements(sql2) {
2250
2332
  return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
@@ -2257,7 +2339,7 @@ function createExecuteSqlToolHandler(sourceId) {
2257
2339
  return async (args, extra) => {
2258
2340
  const { sql: sql2 } = args;
2259
2341
  const startTime = Date.now();
2260
- const effectiveSourceId = sourceId || "default";
2342
+ const effectiveSourceId = getEffectiveSourceId(sourceId);
2261
2343
  let success = true;
2262
2344
  let errorMessage;
2263
2345
  let result;
@@ -2268,18 +2350,18 @@ function createExecuteSqlToolHandler(sourceId) {
2268
2350
  const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
2269
2351
  const isReadonly = toolConfig?.readonly === true;
2270
2352
  if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
2271
- errorMessage = `Read-only mode is enabled for source '${actualSourceId}'. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
2353
+ errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
2272
2354
  success = false;
2273
2355
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
2274
2356
  }
2275
2357
  const executeOptions = {
2276
- readonly: isReadonly,
2277
- max_rows: toolConfig?.max_rows
2358
+ readonly: toolConfig?.readonly,
2359
+ maxRows: toolConfig?.max_rows
2278
2360
  };
2279
2361
  result = await connector.executeSQL(sql2, executeOptions);
2280
2362
  const responseData = {
2281
2363
  rows: result.rows,
2282
- count: result.rows.length,
2364
+ count: result.rowCount,
2283
2365
  source_id: effectiveSourceId
2284
2366
  };
2285
2367
  return createToolSuccessResponse(responseData);
@@ -2288,17 +2370,17 @@ function createExecuteSqlToolHandler(sourceId) {
2288
2370
  errorMessage = error.message;
2289
2371
  return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
2290
2372
  } finally {
2291
- requestStore.add({
2292
- id: crypto.randomUUID(),
2293
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2294
- sourceId: effectiveSourceId,
2295
- toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
2296
- sql: sql2,
2297
- durationMs: Date.now() - startTime,
2298
- client: getClientIdentifier(extra),
2373
+ trackToolRequest(
2374
+ {
2375
+ sourceId: effectiveSourceId,
2376
+ toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
2377
+ sql: sql2
2378
+ },
2379
+ startTime,
2380
+ extra,
2299
2381
  success,
2300
- error: errorMessage
2301
- });
2382
+ errorMessage
2383
+ );
2302
2384
  }
2303
2385
  };
2304
2386
  }
@@ -2306,12 +2388,12 @@ function createExecuteSqlToolHandler(sourceId) {
2306
2388
  // src/tools/search-objects.ts
2307
2389
  import { z as z2 } from "zod";
2308
2390
  var searchDatabaseObjectsSchema = {
2309
- object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Type of database object to search for"),
2310
- pattern: z2.string().optional().default("%").describe("Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."),
2311
- schema: z2.string().optional().describe("Filter results to a specific schema/database (exact match)"),
2312
- table: z2.string().optional().describe("Filter to a specific table (exact match). Requires schema parameter. Only applies to columns and indexes."),
2313
- detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Level of detail to return: names (minimal), summary (with metadata), full (complete structure)"),
2314
- limit: z2.number().int().positive().max(1e3).default(100).describe("Maximum number of results to return (default: 100, max: 1000)")
2391
+ object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Object type to search"),
2392
+ pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
2393
+ schema: z2.string().optional().describe("Filter to schema"),
2394
+ table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
2395
+ detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
2396
+ limit: z2.number().int().positive().max(1e3).default(100).describe("Max results (default: 100, max: 1000)")
2315
2397
  };
2316
2398
  function likePatternToRegex(pattern) {
2317
2399
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
@@ -2585,7 +2667,7 @@ async function searchIndexes(connector, pattern, schemaFilter, tableFilter, deta
2585
2667
  return results;
2586
2668
  }
2587
2669
  function createSearchDatabaseObjectsToolHandler(sourceId) {
2588
- return async (args, _extra) => {
2670
+ return async (args, extra) => {
2589
2671
  const {
2590
2672
  object_type,
2591
2673
  pattern = "%",
@@ -2594,29 +2676,30 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2594
2676
  detail_level = "names",
2595
2677
  limit = 100
2596
2678
  } = args;
2679
+ const startTime = Date.now();
2680
+ const effectiveSourceId = getEffectiveSourceId(sourceId);
2681
+ let success = true;
2682
+ let errorMessage;
2597
2683
  try {
2598
2684
  const connector = ConnectorManager.getCurrentConnector(sourceId);
2599
2685
  if (table) {
2600
2686
  if (!schema) {
2601
- return createToolErrorResponse(
2602
- "The 'table' parameter requires 'schema' to be specified",
2603
- "SCHEMA_REQUIRED"
2604
- );
2687
+ success = false;
2688
+ errorMessage = "The 'table' parameter requires 'schema' to be specified";
2689
+ return createToolErrorResponse(errorMessage, "SCHEMA_REQUIRED");
2605
2690
  }
2606
2691
  if (!["column", "index"].includes(object_type)) {
2607
- return createToolErrorResponse(
2608
- `The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`,
2609
- "INVALID_TABLE_FILTER"
2610
- );
2692
+ success = false;
2693
+ errorMessage = `The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`;
2694
+ return createToolErrorResponse(errorMessage, "INVALID_TABLE_FILTER");
2611
2695
  }
2612
2696
  }
2613
2697
  if (schema) {
2614
2698
  const schemas = await connector.getSchemas();
2615
2699
  if (!schemas.includes(schema)) {
2616
- return createToolErrorResponse(
2617
- `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`,
2618
- "SCHEMA_NOT_FOUND"
2619
- );
2700
+ success = false;
2701
+ errorMessage = `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`;
2702
+ return createToolErrorResponse(errorMessage, "SCHEMA_NOT_FOUND");
2620
2703
  }
2621
2704
  }
2622
2705
  let results = [];
@@ -2637,10 +2720,9 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2637
2720
  results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
2638
2721
  break;
2639
2722
  default:
2640
- return createToolErrorResponse(
2641
- `Unsupported object_type: ${object_type}`,
2642
- "INVALID_OBJECT_TYPE"
2643
- );
2723
+ success = false;
2724
+ errorMessage = `Unsupported object_type: ${object_type}`;
2725
+ return createToolErrorResponse(errorMessage, "INVALID_OBJECT_TYPE");
2644
2726
  }
2645
2727
  return createToolSuccessResponse({
2646
2728
  object_type,
@@ -2653,10 +2735,24 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2653
2735
  truncated: results.length === limit
2654
2736
  });
2655
2737
  } catch (error) {
2738
+ success = false;
2739
+ errorMessage = error.message;
2656
2740
  return createToolErrorResponse(
2657
- `Error searching database objects: ${error.message}`,
2741
+ `Error searching database objects: ${errorMessage}`,
2658
2742
  "SEARCH_ERROR"
2659
2743
  );
2744
+ } finally {
2745
+ trackToolRequest(
2746
+ {
2747
+ sourceId: effectiveSourceId,
2748
+ toolName: effectiveSourceId === "default" ? "search_objects" : `search_objects_${effectiveSourceId}`,
2749
+ sql: `search_objects(object_type=${object_type}, pattern=${pattern}, schema=${schema || "all"}, table=${table || "all"}, detail_level=${detail_level})`
2750
+ },
2751
+ startTime,
2752
+ extra,
2753
+ success,
2754
+ errorMessage
2755
+ );
2660
2756
  }
2661
2757
  };
2662
2758
  }
@@ -2699,14 +2795,19 @@ function zodToParameters(schema) {
2699
2795
  function getExecuteSqlMetadata(sourceId) {
2700
2796
  const sourceIds = ConnectorManager.getAvailableSourceIds();
2701
2797
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
2702
- const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
2703
2798
  const dbType = sourceConfig.type;
2704
- const toolName = sourceId === "default" ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
2705
- const isDefault = sourceIds[0] === sourceId;
2706
- const title = isDefault ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
2799
+ const isSingleSource = sourceIds.length === 1;
2800
+ const registry = getToolRegistry();
2801
+ const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
2802
+ const executeOptions = {
2803
+ readonly: toolConfig?.readonly,
2804
+ maxRows: toolConfig?.max_rows
2805
+ };
2806
+ const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
2807
+ const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
2707
2808
  const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
2708
2809
  const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
2709
- const description = `Execute SQL queries on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}${readonlyNote}${maxRowsNote}`;
2810
+ const description = isSingleSource ? `Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
2710
2811
  const isReadonly = executeOptions.readonly === true;
2711
2812
  const annotations = {
2712
2813
  title,
@@ -2726,10 +2827,14 @@ function getExecuteSqlMetadata(sourceId) {
2726
2827
  annotations
2727
2828
  };
2728
2829
  }
2729
- function getSearchObjectsMetadata(sourceId, dbType, isDefault) {
2730
- const toolName = sourceId === "default" ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
2731
- const title = isDefault ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
2732
- 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.`;
2830
+ function getSearchObjectsMetadata(sourceId) {
2831
+ const sourceIds = ConnectorManager.getAvailableSourceIds();
2832
+ const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
2833
+ const dbType = sourceConfig.type;
2834
+ const isSingleSource = sourceIds.length === 1;
2835
+ const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
2836
+ const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
2837
+ const description = isSingleSource ? `Search and list database objects (schemas, tables, columns, procedures, indexes) on the ${dbType} database` : `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database`;
2733
2838
  return {
2734
2839
  name: toolName,
2735
2840
  description,
@@ -2747,21 +2852,21 @@ function customParamsToToolParams(params) {
2747
2852
  description: param.description
2748
2853
  }));
2749
2854
  }
2750
- function buildExecuteSqlTool(sourceId) {
2855
+ function buildExecuteSqlTool(sourceId, toolConfig) {
2751
2856
  const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
2752
2857
  const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
2858
+ const readonly = toolConfig && "readonly" in toolConfig ? toolConfig.readonly : void 0;
2859
+ const max_rows = toolConfig && "max_rows" in toolConfig ? toolConfig.max_rows : void 0;
2753
2860
  return {
2754
2861
  name: executeSqlMetadata.name,
2755
2862
  description: executeSqlMetadata.description,
2756
- parameters: executeSqlParameters
2863
+ parameters: executeSqlParameters,
2864
+ readonly,
2865
+ max_rows
2757
2866
  };
2758
2867
  }
2759
2868
  function buildSearchObjectsTool(sourceId) {
2760
- const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
2761
- const dbType = sourceConfig.type;
2762
- const sourceIds = ConnectorManager.getAvailableSourceIds();
2763
- const isDefault = sourceIds[0] === sourceId;
2764
- const searchMetadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
2869
+ const searchMetadata = getSearchObjectsMetadata(sourceId);
2765
2870
  return {
2766
2871
  name: searchMetadata.name,
2767
2872
  description: searchMetadata.description,
@@ -2770,40 +2875,51 @@ function buildSearchObjectsTool(sourceId) {
2770
2875
  name: "object_type",
2771
2876
  type: "string",
2772
2877
  required: true,
2773
- description: "Type of database object to search for (schema, table, column, procedure, index)"
2878
+ description: "Object type to search"
2774
2879
  },
2775
2880
  {
2776
2881
  name: "pattern",
2777
2882
  type: "string",
2778
2883
  required: false,
2779
- description: "Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."
2884
+ description: "LIKE pattern (% = any chars, _ = one char). Default: %"
2780
2885
  },
2781
2886
  {
2782
2887
  name: "schema",
2783
2888
  type: "string",
2784
2889
  required: false,
2785
- description: "Filter results to a specific schema/database"
2890
+ description: "Filter to schema"
2891
+ },
2892
+ {
2893
+ name: "table",
2894
+ type: "string",
2895
+ required: false,
2896
+ description: "Filter to table (requires schema; column/index only)"
2786
2897
  },
2787
2898
  {
2788
2899
  name: "detail_level",
2789
2900
  type: "string",
2790
2901
  required: false,
2791
- description: "Level of detail to return: names (minimal), summary (with metadata), full (complete structure). Defaults to 'names'."
2902
+ description: "Detail: names (minimal), summary (metadata), full (all)"
2792
2903
  },
2793
2904
  {
2794
2905
  name: "limit",
2795
2906
  type: "integer",
2796
2907
  required: false,
2797
- description: "Maximum number of results to return (default: 100, max: 1000)"
2908
+ description: "Max results (default: 100, max: 1000)"
2798
2909
  }
2799
- ]
2910
+ ],
2911
+ readonly: true
2912
+ // search_objects is always readonly
2800
2913
  };
2801
2914
  }
2802
2915
  function buildCustomTool(toolConfig) {
2803
2916
  return {
2804
2917
  name: toolConfig.name,
2805
2918
  description: toolConfig.description,
2806
- parameters: customParamsToToolParams(toolConfig.parameters)
2919
+ parameters: customParamsToToolParams(toolConfig.parameters),
2920
+ statement: toolConfig.statement,
2921
+ readonly: toolConfig.readonly,
2922
+ max_rows: toolConfig.max_rows
2807
2923
  };
2808
2924
  }
2809
2925
  function getToolsForSource(sourceId) {
@@ -2811,7 +2927,7 @@ function getToolsForSource(sourceId) {
2811
2927
  const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
2812
2928
  return enabledToolConfigs.map((toolConfig) => {
2813
2929
  if (toolConfig.name === "execute_sql") {
2814
- return buildExecuteSqlTool(sourceId);
2930
+ return buildExecuteSqlTool(sourceId, toolConfig);
2815
2931
  } else if (toolConfig.name === "search_objects") {
2816
2932
  return buildSearchObjectsTool(sourceId);
2817
2933
  } else {
@@ -2878,12 +2994,13 @@ function createCustomToolHandler(toolConfig) {
2878
2994
  try {
2879
2995
  const validatedArgs = zodSchema.parse(args);
2880
2996
  const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
2881
- const executeOptions = ConnectorManager.getCurrentExecuteOptions(
2882
- toolConfig.source
2883
- );
2997
+ const executeOptions = {
2998
+ readonly: toolConfig.readonly,
2999
+ maxRows: toolConfig.max_rows
3000
+ };
2884
3001
  const isReadonly = executeOptions.readonly === true;
2885
3002
  if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
2886
- 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"}`;
3003
+ errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
2887
3004
  success = false;
2888
3005
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
2889
3006
  }
@@ -2898,7 +3015,7 @@ function createCustomToolHandler(toolConfig) {
2898
3015
  );
2899
3016
  const responseData = {
2900
3017
  rows: result.rows,
2901
- count: result.rows.length,
3018
+ count: result.rowCount,
2902
3019
  source_id: toolConfig.source
2903
3020
  };
2904
3021
  return createToolSuccessResponse(responseData);
@@ -2916,17 +3033,17 @@ Parameters: ${JSON.stringify(paramValues)}`;
2916
3033
  }
2917
3034
  return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
2918
3035
  } finally {
2919
- requestStore.add({
2920
- id: crypto.randomUUID(),
2921
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2922
- sourceId: toolConfig.source,
2923
- toolName: toolConfig.name,
2924
- sql: toolConfig.statement,
2925
- durationMs: Date.now() - startTime,
2926
- client: getClientIdentifier(extra),
3036
+ trackToolRequest(
3037
+ {
3038
+ sourceId: toolConfig.source,
3039
+ toolName: toolConfig.name,
3040
+ sql: toolConfig.statement
3041
+ },
3042
+ startTime,
3043
+ extra,
2927
3044
  success,
2928
- error: errorMessage
2929
- });
3045
+ errorMessage
3046
+ );
2930
3047
  }
2931
3048
  };
2932
3049
  }
@@ -2940,21 +3057,18 @@ function registerTools(server) {
2940
3057
  const registry = getToolRegistry();
2941
3058
  for (const sourceId of sourceIds) {
2942
3059
  const enabledTools = registry.getEnabledToolConfigs(sourceId);
2943
- const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
2944
- const dbType = sourceConfig.type;
2945
- const isDefault = sourceIds[0] === sourceId;
2946
3060
  for (const toolConfig of enabledTools) {
2947
3061
  if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
2948
- registerExecuteSqlTool(server, sourceId, dbType);
3062
+ registerExecuteSqlTool(server, sourceId);
2949
3063
  } else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
2950
- registerSearchObjectsTool(server, sourceId, dbType, isDefault);
3064
+ registerSearchObjectsTool(server, sourceId);
2951
3065
  } else {
2952
- registerCustomTool(server, toolConfig, dbType);
3066
+ registerCustomTool(server, sourceId, toolConfig);
2953
3067
  }
2954
3068
  }
2955
3069
  }
2956
3070
  }
2957
- function registerExecuteSqlTool(server, sourceId, dbType) {
3071
+ function registerExecuteSqlTool(server, sourceId) {
2958
3072
  const metadata = getExecuteSqlMetadata(sourceId);
2959
3073
  server.registerTool(
2960
3074
  metadata.name,
@@ -2966,8 +3080,8 @@ function registerExecuteSqlTool(server, sourceId, dbType) {
2966
3080
  createExecuteSqlToolHandler(sourceId)
2967
3081
  );
2968
3082
  }
2969
- function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
2970
- const metadata = getSearchObjectsMetadata(sourceId, dbType, isDefault);
3083
+ function registerSearchObjectsTool(server, sourceId) {
3084
+ const metadata = getSearchObjectsMetadata(sourceId);
2971
3085
  server.registerTool(
2972
3086
  metadata.name,
2973
3087
  {
@@ -2984,7 +3098,9 @@ function registerSearchObjectsTool(server, sourceId, dbType, isDefault) {
2984
3098
  createSearchDatabaseObjectsToolHandler(sourceId)
2985
3099
  );
2986
3100
  }
2987
- function registerCustomTool(server, toolConfig, dbType) {
3101
+ function registerCustomTool(server, sourceId, toolConfig) {
3102
+ const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
3103
+ const dbType = sourceConfig.type;
2988
3104
  const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
2989
3105
  const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
2990
3106
  server.registerTool(
@@ -3002,11 +3118,10 @@ function registerCustomTool(server, toolConfig, dbType) {
3002
3118
  },
3003
3119
  createCustomToolHandler(toolConfig)
3004
3120
  );
3005
- console.error(` - ${toolConfig.name} \u2192 ${toolConfig.source} (${dbType})`);
3006
3121
  }
3007
3122
 
3008
3123
  // src/api/sources.ts
3009
- function transformSourceConfig(source, isDefault) {
3124
+ function transformSourceConfig(source) {
3010
3125
  if (!source.type && source.dsn) {
3011
3126
  const inferredType = getDatabaseTypeFromDSN(source.dsn);
3012
3127
  if (inferredType) {
@@ -3018,8 +3133,7 @@ function transformSourceConfig(source, isDefault) {
3018
3133
  }
3019
3134
  const dataSource = {
3020
3135
  id: source.id,
3021
- type: source.type,
3022
- is_default: isDefault
3136
+ type: source.type
3023
3137
  };
3024
3138
  if (source.host) {
3025
3139
  dataSource.host = source.host;
@@ -3033,12 +3147,6 @@ function transformSourceConfig(source, isDefault) {
3033
3147
  if (source.user) {
3034
3148
  dataSource.user = source.user;
3035
3149
  }
3036
- if (source.readonly !== void 0) {
3037
- dataSource.readonly = source.readonly;
3038
- }
3039
- if (source.max_rows !== void 0) {
3040
- dataSource.max_rows = source.max_rows;
3041
- }
3042
3150
  if (source.ssh_host) {
3043
3151
  const sshTunnel = {
3044
3152
  enabled: true,
@@ -3058,9 +3166,8 @@ function transformSourceConfig(source, isDefault) {
3058
3166
  function listSources(req, res) {
3059
3167
  try {
3060
3168
  const sourceConfigs = ConnectorManager.getAllSourceConfigs();
3061
- const sources = sourceConfigs.map((config, index) => {
3062
- const isDefault = index === 0;
3063
- return transformSourceConfig(config, isDefault);
3169
+ const sources = sourceConfigs.map((config) => {
3170
+ return transformSourceConfig(config);
3064
3171
  });
3065
3172
  res.json(sources);
3066
3173
  } catch (error) {
@@ -3074,7 +3181,6 @@ function listSources(req, res) {
3074
3181
  function getSource(req, res) {
3075
3182
  try {
3076
3183
  const sourceId = req.params.sourceId;
3077
- const sourceIds = ConnectorManager.getAvailableSourceIds();
3078
3184
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
3079
3185
  if (!sourceConfig) {
3080
3186
  const errorResponse = {
@@ -3084,8 +3190,7 @@ function getSource(req, res) {
3084
3190
  res.status(404).json(errorResponse);
3085
3191
  return;
3086
3192
  }
3087
- const isDefault = sourceIds[0] === sourceId;
3088
- const dataSource = transformSourceConfig(sourceConfig, isDefault);
3193
+ const dataSource = transformSourceConfig(sourceConfig);
3089
3194
  res.json(dataSource);
3090
3195
  } catch (error) {
3091
3196
  console.error(`Error getting source ${req.params.sourceId}:`, error);
@@ -3280,13 +3385,8 @@ See documentation for more details on configuring database connections.
3280
3385
  const connectorManager = new ConnectorManager();
3281
3386
  const sources = sourceConfigsData.sources;
3282
3387
  console.error(`Configuration source: ${sourceConfigsData.source}`);
3283
- console.error(`Connecting to ${sources.length} database source(s)...`);
3284
- for (const source of sources) {
3285
- const dsn = source.dsn || buildDSNFromSource(source);
3286
- console.error(` - ${source.id}: ${redactDSN(dsn)}`);
3287
- }
3288
3388
  await connectorManager.connectWithSources(sources);
3289
- const { initializeToolRegistry } = await import("./registry-AWAIN6WO.js");
3389
+ const { initializeToolRegistry } = await import("./registry-XXEL5IXH.js");
3290
3390
  initializeToolRegistry({
3291
3391
  sources: sourceConfigsData.sources,
3292
3392
  tools: sourceConfigsData.tools
@@ -3315,7 +3415,7 @@ See documentation for more details on configuring database connections.
3315
3415
  console.error(generateBanner(SERVER_VERSION, activeModes));
3316
3416
  const sourceDisplayInfos = buildSourceDisplayInfo(
3317
3417
  sources,
3318
- (sourceId) => getToolsForSource(sourceId).map((t) => t.name),
3418
+ (sourceId) => getToolsForSource(sourceId).map((t) => t.readonly ? `\u{1F512} ${t.name}` : t.name),
3319
3419
  isDemo
3320
3420
  );
3321
3421
  console.error(generateStartupTable(sourceDisplayInfos));
@@ -3376,13 +3476,13 @@ See documentation for more details on configuring database connections.
3376
3476
  app.listen(port, "0.0.0.0", () => {
3377
3477
  if (process.env.NODE_ENV === "development") {
3378
3478
  console.error("Development mode detected!");
3379
- console.error(" Admin console dev server (with HMR): http://localhost:5173");
3479
+ console.error(" Workbench dev server (with HMR): http://localhost:5173");
3380
3480
  console.error(" Backend API: http://localhost:8080");
3381
3481
  console.error("");
3382
3482
  } else {
3383
- console.error(`Admin console at http://0.0.0.0:${port}/`);
3483
+ console.error(`Workbench at http://localhost:${port}/`);
3384
3484
  }
3385
- console.error(`MCP server endpoint at http://0.0.0.0:${port}/mcp`);
3485
+ console.error(`MCP server endpoint at http://localhost:${port}/mcp`);
3386
3486
  });
3387
3487
  } else {
3388
3488
  const server = createServer();