@bytebase/dbhub 0.14.0 → 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/README.md CHANGED
@@ -30,7 +30,7 @@
30
30
  MCP Clients MCP Server Databases
31
31
  ```
32
32
 
33
- DBHub is a Minimal Database MCP Server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
33
+ DBHub is a zero-dependency, minimal database MCP server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
34
34
 
35
35
  - **Token Efficient**: Just two general MCP tools (execute_sql, search_objects) to minimize context window usage, plus support for custom tools
36
36
  - **Multi-Database**: Single interface for PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite
@@ -50,6 +50,12 @@ DBHub implements MCP tools for database operations:
50
50
  - **[search_objects](https://dbhub.ai/tools/search-objects)**: Search and explore database schemas, tables, columns, indexes, and procedures with progressive disclosure
51
51
  - **[Custom Tools](https://dbhub.ai/tools/custom-tools)**: Define reusable, parameterized SQL operations in your `dbhub.toml` configuration file
52
52
 
53
+ ## Workbench
54
+
55
+ DBHub includes a [built-in web interface](https://dbhub.ai/workbench/overview) for interacting with your database tools. It provides a visual way to execute queries, run custom tools, and view request traces without requiring an MCP client.
56
+
57
+ ![workbench](https://raw.githubusercontent.com/bytebase/dbhub/main/docs/images/workbench/workbench.webp)
58
+
53
59
  ## Installation
54
60
 
55
61
  See the full [Installation Guide](https://dbhub.ai/installation) for detailed instructions.
@@ -80,13 +86,13 @@ npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:p
80
86
  npx @bytebase/dbhub@latest --transport http --port 8080 --demo
81
87
  ```
82
88
 
83
- See [Server Options](https://dbhub.ai/config/server-options) for all available parameters.
89
+ See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
84
90
 
85
91
  ### Multi-Database Setup
86
92
 
87
93
  Connect to multiple databases simultaneously using TOML configuration files. Perfect for managing production, staging, and development databases from a single DBHub instance.
88
94
 
89
- See [Multi-Database Configuration](https://dbhub.ai/config/multi-database) for complete setup instructions.
95
+ See [Multi-Database Configuration](https://dbhub.ai/config/toml) for complete setup instructions.
90
96
 
91
97
  ## Development
92
98
 
@@ -1237,11 +1237,6 @@ function validateToolsConfig(tools, sources, configPath) {
1237
1237
  `Configuration file ${configPath}: custom tool '${tool.name}' must have 'description' and 'statement' fields`
1238
1238
  );
1239
1239
  }
1240
- if (tool.readonly !== void 0 || tool.max_rows !== void 0) {
1241
- throw new Error(
1242
- `Configuration file ${configPath}: custom tool '${tool.name}' cannot have readonly or max_rows fields (these are only valid for ${BUILTIN_TOOL_EXECUTE_SQL} tool)`
1243
- );
1244
- }
1245
1240
  }
1246
1241
  if (tool.max_rows !== void 0) {
1247
1242
  if (typeof tool.max_rows !== "number" || tool.max_rows <= 0) {
@@ -1250,6 +1245,11 @@ function validateToolsConfig(tools, sources, configPath) {
1250
1245
  );
1251
1246
  }
1252
1247
  }
1248
+ if (tool.readonly !== void 0 && typeof tool.readonly !== "boolean") {
1249
+ throw new Error(
1250
+ `Configuration file ${configPath}: tool '${tool.name}' has invalid readonly. Must be a boolean (true or false).`
1251
+ );
1252
+ }
1253
1253
  }
1254
1254
  }
1255
1255
  function validateSourceConfig(source, configPath) {
@@ -1339,6 +1339,16 @@ function validateSourceConfig(source, configPath) {
1339
1339
  );
1340
1340
  }
1341
1341
  }
1342
+ if (source.readonly !== void 0) {
1343
+ throw new Error(
1344
+ `Configuration file ${configPath}: source '${source.id}' has 'readonly' field, but readonly must be configured per-tool, not per-source. Move 'readonly' to [[tools]] configuration instead.`
1345
+ );
1346
+ }
1347
+ if (source.max_rows !== void 0) {
1348
+ throw new Error(
1349
+ `Configuration file ${configPath}: source '${source.id}' has 'max_rows' field, but max_rows must be configured per-tool, not per-source. Move 'max_rows' to [[tools]] configuration instead.`
1350
+ );
1351
+ }
1342
1352
  }
1343
1353
  function processSourceConfigs(sources, configPath) {
1344
1354
  return sources.map((source) => {
@@ -1446,7 +1456,6 @@ var ConnectorManager = class {
1446
1456
  // Maps for multi-source support
1447
1457
  this.connectors = /* @__PURE__ */ new Map();
1448
1458
  this.sshTunnels = /* @__PURE__ */ new Map();
1449
- this.executeOptions = /* @__PURE__ */ new Map();
1450
1459
  this.sourceConfigs = /* @__PURE__ */ new Map();
1451
1460
  // Store original source configs
1452
1461
  this.sourceIds = [];
@@ -1533,14 +1542,6 @@ var ConnectorManager = class {
1533
1542
  this.connectors.set(sourceId, connector);
1534
1543
  this.sourceIds.push(sourceId);
1535
1544
  this.sourceConfigs.set(sourceId, source);
1536
- const options = {};
1537
- if (source.max_rows !== void 0) {
1538
- options.maxRows = source.max_rows;
1539
- }
1540
- if (source.readonly !== void 0) {
1541
- options.readonly = source.readonly;
1542
- }
1543
- this.executeOptions.set(sourceId, options);
1544
1545
  }
1545
1546
  /**
1546
1547
  * Close all database connections
@@ -1563,7 +1564,6 @@ var ConnectorManager = class {
1563
1564
  }
1564
1565
  this.connectors.clear();
1565
1566
  this.sshTunnels.clear();
1566
- this.executeOptions.clear();
1567
1567
  this.sourceConfigs.clear();
1568
1568
  this.sourceIds = [];
1569
1569
  }
@@ -1608,25 +1608,6 @@ var ConnectorManager = class {
1608
1608
  }
1609
1609
  return managerInstance.getConnector(sourceId);
1610
1610
  }
1611
- /**
1612
- * Get execute options for SQL execution
1613
- * @param sourceId - Optional source ID. If not provided, returns default options
1614
- */
1615
- getExecuteOptions(sourceId) {
1616
- const id = sourceId || this.sourceIds[0];
1617
- return this.executeOptions.get(id) || {};
1618
- }
1619
- /**
1620
- * Get the current execute options
1621
- * This is used by tool handlers
1622
- * @param sourceId - Optional source ID. If not provided, returns default options
1623
- */
1624
- static getCurrentExecuteOptions(sourceId) {
1625
- if (!managerInstance) {
1626
- throw new Error("ConnectorManager not initialized");
1627
- }
1628
- return managerInstance.getExecuteOptions(sourceId);
1629
- }
1630
1611
  /**
1631
1612
  * Get all available source IDs
1632
1613
  */
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  resolveSourceConfigs,
17
17
  resolveTransport,
18
18
  stripCommentsAndStrings
19
- } from "./chunk-WGDSRFBW.js";
19
+ } from "./chunk-TPHNNFR5.js";
20
20
 
21
21
  // src/connectors/postgres/index.ts
22
22
  import pg from "pg";
@@ -477,22 +477,26 @@ var PostgresConnector = class _PostgresConnector {
477
477
  const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
478
478
  if (statements.length === 1) {
479
479
  const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
480
+ let result;
480
481
  if (parameters && parameters.length > 0) {
481
482
  try {
482
- return await client.query(processedStatement, parameters);
483
+ result = await client.query(processedStatement, parameters);
483
484
  } catch (error) {
484
485
  console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
485
486
  console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
486
487
  console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
487
488
  throw error;
488
489
  }
490
+ } else {
491
+ result = await client.query(processedStatement);
489
492
  }
490
- return await client.query(processedStatement);
493
+ return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length };
491
494
  } else {
492
495
  if (parameters && parameters.length > 0) {
493
496
  throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
494
497
  }
495
498
  let allRows = [];
499
+ let totalRowCount = 0;
496
500
  await client.query("BEGIN");
497
501
  try {
498
502
  for (let statement of statements) {
@@ -501,13 +505,16 @@ var PostgresConnector = class _PostgresConnector {
501
505
  if (result.rows && result.rows.length > 0) {
502
506
  allRows.push(...result.rows);
503
507
  }
508
+ if (result.rowCount) {
509
+ totalRowCount += result.rowCount;
510
+ }
504
511
  }
505
512
  await client.query("COMMIT");
506
513
  } catch (error) {
507
514
  await client.query("ROLLBACK");
508
515
  throw error;
509
516
  }
510
- return { rows: allRows };
517
+ return { rows: allRows, rowCount: totalRowCount };
511
518
  }
512
519
  } finally {
513
520
  client.release();
@@ -929,9 +936,6 @@ var SQLServerConnector = class _SQLServerConnector {
929
936
  }
930
937
  return {
931
938
  rows: result.recordset || [],
932
- fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
933
- name: key
934
- })) : [],
935
939
  rowCount: result.rowsAffected[0] || 0
936
940
  };
937
941
  } catch (error) {
@@ -1215,7 +1219,7 @@ var SQLiteConnector = class _SQLiteConnector {
1215
1219
  if (parameters && parameters.length > 0) {
1216
1220
  try {
1217
1221
  const rows = this.db.prepare(processedStatement).all(...parameters);
1218
- return { rows };
1222
+ return { rows, rowCount: rows.length };
1219
1223
  } catch (error) {
1220
1224
  console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
1221
1225
  console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
@@ -1224,12 +1228,13 @@ var SQLiteConnector = class _SQLiteConnector {
1224
1228
  }
1225
1229
  } else {
1226
1230
  const rows = this.db.prepare(processedStatement).all();
1227
- return { rows };
1231
+ return { rows, rowCount: rows.length };
1228
1232
  }
1229
1233
  } else {
1234
+ let result;
1230
1235
  if (parameters && parameters.length > 0) {
1231
1236
  try {
1232
- this.db.prepare(processedStatement).run(...parameters);
1237
+ result = this.db.prepare(processedStatement).run(...parameters);
1233
1238
  } catch (error) {
1234
1239
  console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
1235
1240
  console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
@@ -1237,9 +1242,9 @@ var SQLiteConnector = class _SQLiteConnector {
1237
1242
  throw error;
1238
1243
  }
1239
1244
  } else {
1240
- this.db.prepare(processedStatement).run();
1245
+ result = this.db.prepare(processedStatement).run();
1241
1246
  }
1242
- return { rows: [] };
1247
+ return { rows: [], rowCount: result.changes };
1243
1248
  }
1244
1249
  } else {
1245
1250
  if (parameters && parameters.length > 0) {
@@ -1255,8 +1260,10 @@ var SQLiteConnector = class _SQLiteConnector {
1255
1260
  writeStatements.push(statement);
1256
1261
  }
1257
1262
  }
1258
- if (writeStatements.length > 0) {
1259
- 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;
1260
1267
  }
1261
1268
  let allRows = [];
1262
1269
  for (let statement of readStatements) {
@@ -1264,7 +1271,7 @@ var SQLiteConnector = class _SQLiteConnector {
1264
1271
  const result = this.db.prepare(statement).all();
1265
1272
  allRows.push(...result);
1266
1273
  }
1267
- return { rows: allRows };
1274
+ return { rows: allRows, rowCount: totalChanges + allRows.length };
1268
1275
  }
1269
1276
  } catch (error) {
1270
1277
  throw error;
@@ -1303,6 +1310,26 @@ function extractRowsFromMultiStatement(results) {
1303
1310
  }
1304
1311
  return allRows;
1305
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
+ }
1306
1333
  function parseQueryResults(results) {
1307
1334
  if (!Array.isArray(results)) {
1308
1335
  return [];
@@ -1716,7 +1743,8 @@ var MySQLConnector = class _MySQLConnector {
1716
1743
  }
1717
1744
  const [firstResult] = results;
1718
1745
  const rows = parseQueryResults(firstResult);
1719
- return { rows };
1746
+ const rowCount = extractAffectedRows(firstResult);
1747
+ return { rows, rowCount };
1720
1748
  } catch (error) {
1721
1749
  console.error("Error executing query:", error);
1722
1750
  throw error;
@@ -2127,7 +2155,8 @@ var MariaDBConnector = class _MariaDBConnector {
2127
2155
  results = await conn.query(processedSQL);
2128
2156
  }
2129
2157
  const rows = parseQueryResults(results);
2130
- return { rows };
2158
+ const rowCount = extractAffectedRows(results);
2159
+ return { rows, rowCount };
2131
2160
  } catch (error) {
2132
2161
  console.error("Error executing query:", error);
2133
2162
  throw error;
@@ -2274,6 +2303,27 @@ function getClientIdentifier(extra) {
2274
2303
  return "stdio";
2275
2304
  }
2276
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
+
2277
2327
  // src/tools/execute-sql.ts
2278
2328
  var executeSqlSchema = {
2279
2329
  sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
@@ -2289,7 +2339,7 @@ function createExecuteSqlToolHandler(sourceId) {
2289
2339
  return async (args, extra) => {
2290
2340
  const { sql: sql2 } = args;
2291
2341
  const startTime = Date.now();
2292
- const effectiveSourceId = sourceId || "default";
2342
+ const effectiveSourceId = getEffectiveSourceId(sourceId);
2293
2343
  let success = true;
2294
2344
  let errorMessage;
2295
2345
  let result;
@@ -2300,18 +2350,18 @@ function createExecuteSqlToolHandler(sourceId) {
2300
2350
  const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
2301
2351
  const isReadonly = toolConfig?.readonly === true;
2302
2352
  if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
2303
- 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"}`;
2304
2354
  success = false;
2305
2355
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
2306
2356
  }
2307
2357
  const executeOptions = {
2308
- readonly: isReadonly,
2309
- max_rows: toolConfig?.max_rows
2358
+ readonly: toolConfig?.readonly,
2359
+ maxRows: toolConfig?.max_rows
2310
2360
  };
2311
2361
  result = await connector.executeSQL(sql2, executeOptions);
2312
2362
  const responseData = {
2313
2363
  rows: result.rows,
2314
- count: result.rows.length,
2364
+ count: result.rowCount,
2315
2365
  source_id: effectiveSourceId
2316
2366
  };
2317
2367
  return createToolSuccessResponse(responseData);
@@ -2320,17 +2370,17 @@ function createExecuteSqlToolHandler(sourceId) {
2320
2370
  errorMessage = error.message;
2321
2371
  return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
2322
2372
  } finally {
2323
- requestStore.add({
2324
- id: crypto.randomUUID(),
2325
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2326
- sourceId: effectiveSourceId,
2327
- toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
2328
- sql: sql2,
2329
- durationMs: Date.now() - startTime,
2330
- 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,
2331
2381
  success,
2332
- error: errorMessage
2333
- });
2382
+ errorMessage
2383
+ );
2334
2384
  }
2335
2385
  };
2336
2386
  }
@@ -2617,7 +2667,7 @@ async function searchIndexes(connector, pattern, schemaFilter, tableFilter, deta
2617
2667
  return results;
2618
2668
  }
2619
2669
  function createSearchDatabaseObjectsToolHandler(sourceId) {
2620
- return async (args, _extra) => {
2670
+ return async (args, extra) => {
2621
2671
  const {
2622
2672
  object_type,
2623
2673
  pattern = "%",
@@ -2626,29 +2676,30 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2626
2676
  detail_level = "names",
2627
2677
  limit = 100
2628
2678
  } = args;
2679
+ const startTime = Date.now();
2680
+ const effectiveSourceId = getEffectiveSourceId(sourceId);
2681
+ let success = true;
2682
+ let errorMessage;
2629
2683
  try {
2630
2684
  const connector = ConnectorManager.getCurrentConnector(sourceId);
2631
2685
  if (table) {
2632
2686
  if (!schema) {
2633
- return createToolErrorResponse(
2634
- "The 'table' parameter requires 'schema' to be specified",
2635
- "SCHEMA_REQUIRED"
2636
- );
2687
+ success = false;
2688
+ errorMessage = "The 'table' parameter requires 'schema' to be specified";
2689
+ return createToolErrorResponse(errorMessage, "SCHEMA_REQUIRED");
2637
2690
  }
2638
2691
  if (!["column", "index"].includes(object_type)) {
2639
- return createToolErrorResponse(
2640
- `The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`,
2641
- "INVALID_TABLE_FILTER"
2642
- );
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");
2643
2695
  }
2644
2696
  }
2645
2697
  if (schema) {
2646
2698
  const schemas = await connector.getSchemas();
2647
2699
  if (!schemas.includes(schema)) {
2648
- return createToolErrorResponse(
2649
- `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`,
2650
- "SCHEMA_NOT_FOUND"
2651
- );
2700
+ success = false;
2701
+ errorMessage = `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`;
2702
+ return createToolErrorResponse(errorMessage, "SCHEMA_NOT_FOUND");
2652
2703
  }
2653
2704
  }
2654
2705
  let results = [];
@@ -2669,10 +2720,9 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2669
2720
  results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
2670
2721
  break;
2671
2722
  default:
2672
- return createToolErrorResponse(
2673
- `Unsupported object_type: ${object_type}`,
2674
- "INVALID_OBJECT_TYPE"
2675
- );
2723
+ success = false;
2724
+ errorMessage = `Unsupported object_type: ${object_type}`;
2725
+ return createToolErrorResponse(errorMessage, "INVALID_OBJECT_TYPE");
2676
2726
  }
2677
2727
  return createToolSuccessResponse({
2678
2728
  object_type,
@@ -2685,10 +2735,24 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2685
2735
  truncated: results.length === limit
2686
2736
  });
2687
2737
  } catch (error) {
2738
+ success = false;
2739
+ errorMessage = error.message;
2688
2740
  return createToolErrorResponse(
2689
- `Error searching database objects: ${error.message}`,
2741
+ `Error searching database objects: ${errorMessage}`,
2690
2742
  "SEARCH_ERROR"
2691
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
+ );
2692
2756
  }
2693
2757
  };
2694
2758
  }
@@ -2731,9 +2795,14 @@ function zodToParameters(schema) {
2731
2795
  function getExecuteSqlMetadata(sourceId) {
2732
2796
  const sourceIds = ConnectorManager.getAvailableSourceIds();
2733
2797
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
2734
- const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
2735
2798
  const dbType = sourceConfig.type;
2736
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
+ };
2737
2806
  const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
2738
2807
  const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
2739
2808
  const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
@@ -2783,13 +2852,17 @@ function customParamsToToolParams(params) {
2783
2852
  description: param.description
2784
2853
  }));
2785
2854
  }
2786
- function buildExecuteSqlTool(sourceId) {
2855
+ function buildExecuteSqlTool(sourceId, toolConfig) {
2787
2856
  const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
2788
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;
2789
2860
  return {
2790
2861
  name: executeSqlMetadata.name,
2791
2862
  description: executeSqlMetadata.description,
2792
- parameters: executeSqlParameters
2863
+ parameters: executeSqlParameters,
2864
+ readonly,
2865
+ max_rows
2793
2866
  };
2794
2867
  }
2795
2868
  function buildSearchObjectsTool(sourceId) {
@@ -2834,14 +2907,19 @@ function buildSearchObjectsTool(sourceId) {
2834
2907
  required: false,
2835
2908
  description: "Max results (default: 100, max: 1000)"
2836
2909
  }
2837
- ]
2910
+ ],
2911
+ readonly: true
2912
+ // search_objects is always readonly
2838
2913
  };
2839
2914
  }
2840
2915
  function buildCustomTool(toolConfig) {
2841
2916
  return {
2842
2917
  name: toolConfig.name,
2843
2918
  description: toolConfig.description,
2844
- parameters: customParamsToToolParams(toolConfig.parameters)
2919
+ parameters: customParamsToToolParams(toolConfig.parameters),
2920
+ statement: toolConfig.statement,
2921
+ readonly: toolConfig.readonly,
2922
+ max_rows: toolConfig.max_rows
2845
2923
  };
2846
2924
  }
2847
2925
  function getToolsForSource(sourceId) {
@@ -2849,7 +2927,7 @@ function getToolsForSource(sourceId) {
2849
2927
  const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
2850
2928
  return enabledToolConfigs.map((toolConfig) => {
2851
2929
  if (toolConfig.name === "execute_sql") {
2852
- return buildExecuteSqlTool(sourceId);
2930
+ return buildExecuteSqlTool(sourceId, toolConfig);
2853
2931
  } else if (toolConfig.name === "search_objects") {
2854
2932
  return buildSearchObjectsTool(sourceId);
2855
2933
  } else {
@@ -2916,12 +2994,13 @@ function createCustomToolHandler(toolConfig) {
2916
2994
  try {
2917
2995
  const validatedArgs = zodSchema.parse(args);
2918
2996
  const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
2919
- const executeOptions = ConnectorManager.getCurrentExecuteOptions(
2920
- toolConfig.source
2921
- );
2997
+ const executeOptions = {
2998
+ readonly: toolConfig.readonly,
2999
+ maxRows: toolConfig.max_rows
3000
+ };
2922
3001
  const isReadonly = executeOptions.readonly === true;
2923
3002
  if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
2924
- 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);
2925
3004
  success = false;
2926
3005
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
2927
3006
  }
@@ -2936,7 +3015,7 @@ function createCustomToolHandler(toolConfig) {
2936
3015
  );
2937
3016
  const responseData = {
2938
3017
  rows: result.rows,
2939
- count: result.rows.length,
3018
+ count: result.rowCount,
2940
3019
  source_id: toolConfig.source
2941
3020
  };
2942
3021
  return createToolSuccessResponse(responseData);
@@ -2954,17 +3033,17 @@ Parameters: ${JSON.stringify(paramValues)}`;
2954
3033
  }
2955
3034
  return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
2956
3035
  } finally {
2957
- requestStore.add({
2958
- id: crypto.randomUUID(),
2959
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2960
- sourceId: toolConfig.source,
2961
- toolName: toolConfig.name,
2962
- sql: toolConfig.statement,
2963
- durationMs: Date.now() - startTime,
2964
- client: getClientIdentifier(extra),
3036
+ trackToolRequest(
3037
+ {
3038
+ sourceId: toolConfig.source,
3039
+ toolName: toolConfig.name,
3040
+ sql: toolConfig.statement
3041
+ },
3042
+ startTime,
3043
+ extra,
2965
3044
  success,
2966
- error: errorMessage
2967
- });
3045
+ errorMessage
3046
+ );
2968
3047
  }
2969
3048
  };
2970
3049
  }
@@ -3039,7 +3118,6 @@ function registerCustomTool(server, sourceId, toolConfig) {
3039
3118
  },
3040
3119
  createCustomToolHandler(toolConfig)
3041
3120
  );
3042
- console.error(` - ${toolConfig.name} \u2192 ${toolConfig.source} (${dbType})`);
3043
3121
  }
3044
3122
 
3045
3123
  // src/api/sources.ts
@@ -3069,12 +3147,6 @@ function transformSourceConfig(source) {
3069
3147
  if (source.user) {
3070
3148
  dataSource.user = source.user;
3071
3149
  }
3072
- if (source.readonly !== void 0) {
3073
- dataSource.readonly = source.readonly;
3074
- }
3075
- if (source.max_rows !== void 0) {
3076
- dataSource.max_rows = source.max_rows;
3077
- }
3078
3150
  if (source.ssh_host) {
3079
3151
  const sshTunnel = {
3080
3152
  enabled: true,
@@ -3314,7 +3386,7 @@ See documentation for more details on configuring database connections.
3314
3386
  const sources = sourceConfigsData.sources;
3315
3387
  console.error(`Configuration source: ${sourceConfigsData.source}`);
3316
3388
  await connectorManager.connectWithSources(sources);
3317
- const { initializeToolRegistry } = await import("./registry-FVGT25UH.js");
3389
+ const { initializeToolRegistry } = await import("./registry-XXEL5IXH.js");
3318
3390
  initializeToolRegistry({
3319
3391
  sources: sourceConfigsData.sources,
3320
3392
  tools: sourceConfigsData.tools
@@ -3343,7 +3415,7 @@ See documentation for more details on configuring database connections.
3343
3415
  console.error(generateBanner(SERVER_VERSION, activeModes));
3344
3416
  const sourceDisplayInfos = buildSourceDisplayInfo(
3345
3417
  sources,
3346
- (sourceId) => getToolsForSource(sourceId).map((t) => t.name),
3418
+ (sourceId) => getToolsForSource(sourceId).map((t) => t.readonly ? `\u{1F512} ${t.name}` : t.name),
3347
3419
  isDemo
3348
3420
  );
3349
3421
  console.error(generateStartupTable(sourceDisplayInfos));
@@ -3404,11 +3476,11 @@ See documentation for more details on configuring database connections.
3404
3476
  app.listen(port, "0.0.0.0", () => {
3405
3477
  if (process.env.NODE_ENV === "development") {
3406
3478
  console.error("Development mode detected!");
3407
- console.error(" Admin console dev server (with HMR): http://localhost:5173");
3479
+ console.error(" Workbench dev server (with HMR): http://localhost:5173");
3408
3480
  console.error(" Backend API: http://localhost:8080");
3409
3481
  console.error("");
3410
3482
  } else {
3411
- console.error(`Admin console at http://localhost:${port}/`);
3483
+ console.error(`Workbench at http://localhost:${port}/`);
3412
3484
  }
3413
3485
  console.error(`MCP server endpoint at http://localhost:${port}/mcp`);
3414
3486
  });