@bytebase/dbhub 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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 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:
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
 
@@ -1451,7 +1451,7 @@ function buildDSNFromSource(source) {
1451
1451
  // src/connectors/manager.ts
1452
1452
  var managerInstance = null;
1453
1453
  var ConnectorManager = class {
1454
- // Ordered list of source IDs (first is default)
1454
+ // Prevent race conditions
1455
1455
  constructor() {
1456
1456
  // Maps for multi-source support
1457
1457
  this.connectors = /* @__PURE__ */ new Map();
@@ -1459,6 +1459,11 @@ var ConnectorManager = class {
1459
1459
  this.sourceConfigs = /* @__PURE__ */ new Map();
1460
1460
  // Store original source configs
1461
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();
1462
1467
  if (!managerInstance) {
1463
1468
  managerInstance = this;
1464
1469
  }
@@ -1471,10 +1476,73 @@ var ConnectorManager = class {
1471
1476
  if (sources.length === 0) {
1472
1477
  throw new Error("No sources provided");
1473
1478
  }
1474
- console.error(`Connecting to ${sources.length} database source(s)...`);
1475
- 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) {
1476
1485
  await this.connectSource(source);
1477
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);
1478
1546
  }
1479
1547
  /**
1480
1548
  * Connect to a single source (helper for connectWithSources)
@@ -1540,7 +1608,9 @@ var ConnectorManager = class {
1540
1608
  }
1541
1609
  await connector.connect(actualDSN, source.init_script, config);
1542
1610
  this.connectors.set(sourceId, connector);
1543
- this.sourceIds.push(sourceId);
1611
+ if (!this.sourceIds.includes(sourceId)) {
1612
+ this.sourceIds.push(sourceId);
1613
+ }
1544
1614
  this.sourceConfigs.set(sourceId, source);
1545
1615
  }
1546
1616
  /**
@@ -1565,6 +1635,8 @@ var ConnectorManager = class {
1565
1635
  this.connectors.clear();
1566
1636
  this.sshTunnels.clear();
1567
1637
  this.sourceConfigs.clear();
1638
+ this.lazySources.clear();
1639
+ this.pendingConnections.clear();
1568
1640
  this.sourceIds = [];
1569
1641
  }
1570
1642
  /**
@@ -1626,7 +1698,7 @@ var ConnectorManager = class {
1626
1698
  * @param sourceId - Source ID. If not provided, returns default (first) source config
1627
1699
  */
1628
1700
  getSourceConfig(sourceId) {
1629
- if (this.connectors.size === 0) {
1701
+ if (this.sourceIds.length === 0) {
1630
1702
  return null;
1631
1703
  }
1632
1704
  const id = sourceId || this.sourceIds[0];
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  resolveSourceConfigs,
17
17
  resolveTransport,
18
18
  stripCommentsAndStrings
19
- } from "./chunk-TPHNNFR5.js";
19
+ } from "./chunk-IBBG4PSO.js";
20
20
 
21
21
  // src/connectors/postgres/index.ts
22
22
  import pg from "pg";
@@ -1757,7 +1757,7 @@ var mysqlConnector = new MySQLConnector();
1757
1757
  ConnectorRegistry.register(mysqlConnector);
1758
1758
 
1759
1759
  // src/connectors/mariadb/index.ts
1760
- import mariadb from "mariadb";
1760
+ import * as mariadb from "mariadb";
1761
1761
  var MariadbDSNParser = class {
1762
1762
  async parse(dsn, config) {
1763
1763
  const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
@@ -2344,6 +2344,7 @@ function createExecuteSqlToolHandler(sourceId) {
2344
2344
  let errorMessage;
2345
2345
  let result;
2346
2346
  try {
2347
+ await ConnectorManager.ensureConnected(sourceId);
2347
2348
  const connector = ConnectorManager.getCurrentConnector(sourceId);
2348
2349
  const actualSourceId = connector.getId();
2349
2350
  const registry = getToolRegistry();
@@ -2681,6 +2682,7 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
2681
2682
  let success = true;
2682
2683
  let errorMessage;
2683
2684
  try {
2685
+ await ConnectorManager.ensureConnected(sourceId);
2684
2686
  const connector = ConnectorManager.getCurrentConnector(sourceId);
2685
2687
  if (table) {
2686
2688
  if (!schema) {
@@ -2993,6 +2995,7 @@ function createCustomToolHandler(toolConfig) {
2993
2995
  let paramValues = [];
2994
2996
  try {
2995
2997
  const validatedArgs = zodSchema.parse(args);
2998
+ await ConnectorManager.ensureConnected(toolConfig.source);
2996
2999
  const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
2997
3000
  const executeOptions = {
2998
3001
  readonly: toolConfig.readonly,
@@ -3135,6 +3138,9 @@ function transformSourceConfig(source) {
3135
3138
  id: source.id,
3136
3139
  type: source.type
3137
3140
  };
3141
+ if (source.description) {
3142
+ dataSource.description = source.description;
3143
+ }
3138
3144
  if (source.host) {
3139
3145
  dataSource.host = source.host;
3140
3146
  }
@@ -3386,7 +3392,7 @@ See documentation for more details on configuring database connections.
3386
3392
  const sources = sourceConfigsData.sources;
3387
3393
  console.error(`Configuration source: ${sourceConfigsData.source}`);
3388
3394
  await connectorManager.connectWithSources(sources);
3389
- const { initializeToolRegistry } = await import("./registry-XXEL5IXH.js");
3395
+ const { initializeToolRegistry } = await import("./registry-D77Y4CIA.js");
3390
3396
  initializeToolRegistry({
3391
3397
  sources: sourceConfigsData.sources,
3392
3398
  tools: sourceConfigsData.tools
@@ -3424,9 +3430,6 @@ See documentation for more details on configuring database connections.
3424
3430
  app.use(express.json());
3425
3431
  app.use((req, res, next) => {
3426
3432
  const origin = req.headers.origin;
3427
- if (origin && !origin.startsWith("http://localhost") && !origin.startsWith("https://localhost")) {
3428
- return res.status(403).json({ error: "Forbidden origin" });
3429
- }
3430
3433
  res.header("Access-Control-Allow-Origin", origin || "http://localhost");
3431
3434
  res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
3432
3435
  res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
@@ -2,7 +2,7 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-TPHNNFR5.js";
5
+ } from "./chunk-IBBG4PSO.js";
6
6
  export {
7
7
  ToolRegistry,
8
8
  getToolRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "mcpName": "io.github.bytebase/dbhub",
5
5
  "description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
6
6
  "repository": {