@bytebase/dbhub 0.14.0 → 0.16.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
@@ -23,20 +23,20 @@
23
23
  | | | | | |
24
24
  | VS Code +--->+ +--->+ MySQL |
25
25
  | | | | | |
26
- | Other Clients +--->+ +--->+ MariaDB |
26
+ | Copilot CLI +--->+ +--->+ MariaDB |
27
27
  | | | | | |
28
28
  | | | | | |
29
29
  +------------------+ +--------------+ +------------------+
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, token efficient 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
- - **Token Efficient**: Just two general MCP tools (execute_sql, search_objects) to minimize context window usage, plus support for custom tools
36
- - **Multi-Database**: Single interface for PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite
37
- - **Secure Access**: Read-only mode, SSH tunneling, and SSL/TLS encryption support
38
- - **Multiple Connections**: Connect to multiple databases simultaneously with TOML configuration
39
- - **Production-Ready**: Row limiting, lock timeout control, and connection pooling
35
+ - **Local Development First**: Zero dependency, token efficient with just two MCP tools to maximize context window
36
+ - **Multi-Database**: PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite through a single interface
37
+ - **Multi-Connection**: Connect to multiple databases simultaneously with TOML configuration
38
+ - **Guardrails**: Read-only mode, row limiting, and query timeout to prevent runaway operations
39
+ - **Secure Access**: SSH tunneling and SSL/TLS encryption
40
40
 
41
41
  ## Supported Databases
42
42
 
@@ -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) => {
@@ -1441,15 +1451,19 @@ function buildDSNFromSource(source) {
1441
1451
  // src/connectors/manager.ts
1442
1452
  var managerInstance = null;
1443
1453
  var ConnectorManager = class {
1444
- // Ordered list of source IDs (first is default)
1454
+ // Prevent race conditions
1445
1455
  constructor() {
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 = [];
1462
+ // Ordered list of source IDs (first is default)
1463
+ // Lazy connection support
1464
+ this.lazySources = /* @__PURE__ */ new Map();
1465
+ // Sources pending lazy connection
1466
+ this.pendingConnections = /* @__PURE__ */ new Map();
1453
1467
  if (!managerInstance) {
1454
1468
  managerInstance = this;
1455
1469
  }
@@ -1462,10 +1476,73 @@ var ConnectorManager = class {
1462
1476
  if (sources.length === 0) {
1463
1477
  throw new Error("No sources provided");
1464
1478
  }
1465
- console.error(`Connecting to ${sources.length} database source(s)...`);
1466
- for (const source of sources) {
1479
+ const eagerSources = sources.filter((s) => !s.lazy);
1480
+ const lazySources = sources.filter((s) => s.lazy);
1481
+ if (eagerSources.length > 0) {
1482
+ console.error(`Connecting to ${eagerSources.length} database source(s)...`);
1483
+ }
1484
+ for (const source of eagerSources) {
1467
1485
  await this.connectSource(source);
1468
1486
  }
1487
+ for (const source of lazySources) {
1488
+ this.registerLazySource(source);
1489
+ }
1490
+ }
1491
+ /**
1492
+ * Register a lazy source without establishing connection
1493
+ * Connection will be established on first use via ensureConnected()
1494
+ */
1495
+ registerLazySource(source) {
1496
+ const sourceId = source.id;
1497
+ const dsn = buildDSNFromSource(source);
1498
+ console.error(` - ${sourceId}: ${redactDSN(dsn)} (lazy, will connect on first use)`);
1499
+ this.lazySources.set(sourceId, source);
1500
+ this.sourceConfigs.set(sourceId, source);
1501
+ this.sourceIds.push(sourceId);
1502
+ }
1503
+ /**
1504
+ * Ensure a source is connected (handles lazy connection on demand)
1505
+ * Safe to call multiple times - uses promise-based deduplication so concurrent calls share the same connection attempt
1506
+ */
1507
+ async ensureConnected(sourceId) {
1508
+ const id = sourceId || this.sourceIds[0];
1509
+ if (this.connectors.has(id)) {
1510
+ return;
1511
+ }
1512
+ const lazySource = this.lazySources.get(id);
1513
+ if (!lazySource) {
1514
+ if (sourceId) {
1515
+ throw new Error(
1516
+ `Source '${sourceId}' not found. Available sources: ${this.sourceIds.join(", ")}`
1517
+ );
1518
+ } else {
1519
+ throw new Error("No sources configured. Call connectWithSources() first.");
1520
+ }
1521
+ }
1522
+ const pending = this.pendingConnections.get(id);
1523
+ if (pending) {
1524
+ return pending;
1525
+ }
1526
+ const connectionPromise = (async () => {
1527
+ try {
1528
+ console.error(`Lazy connecting to source '${id}'...`);
1529
+ await this.connectSource(lazySource);
1530
+ this.lazySources.delete(id);
1531
+ } finally {
1532
+ this.pendingConnections.delete(id);
1533
+ }
1534
+ })();
1535
+ this.pendingConnections.set(id, connectionPromise);
1536
+ return connectionPromise;
1537
+ }
1538
+ /**
1539
+ * Static method to ensure a source is connected (for tool handlers)
1540
+ */
1541
+ static async ensureConnected(sourceId) {
1542
+ if (!managerInstance) {
1543
+ throw new Error("ConnectorManager not initialized");
1544
+ }
1545
+ return managerInstance.ensureConnected(sourceId);
1469
1546
  }
1470
1547
  /**
1471
1548
  * Connect to a single source (helper for connectWithSources)
@@ -1531,16 +1608,10 @@ var ConnectorManager = class {
1531
1608
  }
1532
1609
  await connector.connect(actualDSN, source.init_script, config);
1533
1610
  this.connectors.set(sourceId, connector);
1534
- this.sourceIds.push(sourceId);
1535
- 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;
1611
+ if (!this.sourceIds.includes(sourceId)) {
1612
+ this.sourceIds.push(sourceId);
1542
1613
  }
1543
- this.executeOptions.set(sourceId, options);
1614
+ this.sourceConfigs.set(sourceId, source);
1544
1615
  }
1545
1616
  /**
1546
1617
  * Close all database connections
@@ -1563,8 +1634,9 @@ var ConnectorManager = class {
1563
1634
  }
1564
1635
  this.connectors.clear();
1565
1636
  this.sshTunnels.clear();
1566
- this.executeOptions.clear();
1567
1637
  this.sourceConfigs.clear();
1638
+ this.lazySources.clear();
1639
+ this.pendingConnections.clear();
1568
1640
  this.sourceIds = [];
1569
1641
  }
1570
1642
  /**
@@ -1608,25 +1680,6 @@ var ConnectorManager = class {
1608
1680
  }
1609
1681
  return managerInstance.getConnector(sourceId);
1610
1682
  }
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
1683
  /**
1631
1684
  * Get all available source IDs
1632
1685
  */
@@ -1645,7 +1698,7 @@ var ConnectorManager = class {
1645
1698
  * @param sourceId - Source ID. If not provided, returns default (first) source config
1646
1699
  */
1647
1700
  getSourceConfig(sourceId) {
1648
- if (this.connectors.size === 0) {
1701
+ if (this.sourceIds.length === 0) {
1649
1702
  return null;
1650
1703
  }
1651
1704
  const id = sourceId || this.sourceIds[0];