@bytebase/dbhub 0.2.3 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +36 -23
  2. package/dist/index.js +450 -83
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <p align="center">
2
2
  <a href="https://dbhub.ai/" target="_blank">
3
3
  <picture>
4
- <img src="https://raw.githubusercontent.com/bytebase/dbhub/main/resources/images/logo-full.svg" width="50%">
4
+ <img src="https://raw.githubusercontent.com/bytebase/dbhub/main/resources/images/logo-full.webp" width="50%">
5
5
  </picture>
6
6
  </a>
7
7
  </p>
@@ -20,7 +20,7 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
20
20
  | Clients | | | | |
21
21
  | | | +--->+ MySQL |
22
22
  | | | | | |
23
- | | | +--->+ Other Databases |
23
+ | | | +--->+ MariaDB |
24
24
  | | | | | |
25
25
  +------------------+ +--------------+ +------------------+
26
26
  MCP Clients MCP Server Databases
@@ -36,28 +36,28 @@ https://demo.dbhub.ai/sse connects a [sample employee database](https://github.c
36
36
 
37
37
  ### Database Resources
38
38
 
39
- | Resource Name | URI Format | PostgreSQL | MySQL | SQL Server | SQLite |
40
- | --------------------------- | ------------------------------------------------------ | :--------: | :---: | :--------: | :----: |
41
- | schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ |
42
- | tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ |
43
- | table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ |
44
- | indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ |
45
- | procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ❌ |
46
- | procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ❌ |
39
+ | Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
40
+ | --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: | :----: |
41
+ | schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ | ✅ |
42
+ | tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ | ✅ |
43
+ | table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ | ✅ |
44
+ | indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ | ✅ |
45
+ | procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ | ❌ |
46
+ | procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ | ❌ |
47
47
 
48
48
  ### Database Tools
49
49
 
50
- | Tool | Command Name | PostgreSQL | MySQL | SQL Server | SQLite |
51
- | --------------- | ----------------- | :--------: | :---: | :--------: | :----: |
52
- | Execute Query | `run_query` | ✅ | ✅ | ✅ | |
53
- | List Connectors | `list_connectors` | ✅ | ✅ | ✅ | |
50
+ | Tool | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
51
+ | --------------- | ----------------- | :--------: | :---: | :-----: | :--------: | ------ |
52
+ | Execute Query | `run_query` | ✅ | ✅ | ✅ | ✅ | |
53
+ | List Connectors | `list_connectors` | ✅ | ✅ | ✅ | ✅ | |
54
54
 
55
55
  ### Prompt Capabilities
56
56
 
57
- | Prompt | Command Name | PostgreSQL | MySQL | SQL Server | SQLite |
58
- | ------------------- | -------------- | :--------: | :---: | :--------: | :----: |
59
- | Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | |
60
- | Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | |
57
+ | Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
58
+ | ------------------- | -------------- | :--------: | :---: | :-----: | :--------: | ------ |
59
+ | Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ | |
60
+ | Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ | |
61
61
 
62
62
  ## Installation
63
63
 
@@ -156,7 +156,7 @@ npx @bytebase/dbhub --transport sse --port 8080 --demo
156
156
  You can use DBHub in demo mode with a sample employee database for testing:
157
157
 
158
158
  ```bash
159
- pnpm dev --demo
159
+ npx @bytebase/dbhub --demo
160
160
  ```
161
161
 
162
162
  For real databases, a Database Source Name (DSN) is required. You can provide this in several ways:
@@ -164,14 +164,14 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
164
164
  - **Command line argument** (highest priority):
165
165
 
166
166
  ```bash
167
- pnpm dev --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
167
+ npx @bytebase/dbhub --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
168
168
  ```
169
169
 
170
170
  - **Environment variable** (second priority):
171
171
 
172
172
  ```bash
173
173
  export DSN="postgres://user:password@localhost:5432/dbname?sslmode=disable"
174
- pnpm dev
174
+ npx @bytebase/dbhub
175
175
  ```
176
176
 
177
177
  - **Environment file** (third priority):
@@ -188,10 +188,19 @@ DBHub supports the following database connection string formats:
188
188
 
189
189
  | Database | DSN Format | Example |
190
190
  | ---------- | -------------------------------------------------------- | ---------------------------------------------------------------- |
191
+ | MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname` |
192
+ | MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname` |
191
193
  | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
192
- | SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db` or `sqlite::memory:` |
193
194
  | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname` |
194
- | MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname` |
195
+ | SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite::memory:` |
196
+
197
+ #### SQL Server
198
+
199
+ Extra query parameters:
200
+
201
+ #### authentication
202
+
203
+ - `authentication=azure-active-directory-access-token`. Only applicable when running from Azure. See [DefaultAzureCredential](https://learn.microsoft.com/en-us/azure/developer/javascript/sdk/authentication/credential-chains#use-defaultazurecredential-for-flexibility).
195
204
 
196
205
  ### Transport
197
206
 
@@ -257,3 +266,7 @@ npx @modelcontextprotocol/inspector
257
266
  ```
258
267
 
259
268
  Connect to the DBHub server `/sse` endpoint
269
+
270
+ ## Star History
271
+
272
+ [![Star History Chart](https://api.star-history.com/svg?repos=bytebase/dbhub&type=Date)](https://www.star-history.com/#bytebase/dbhub&Date)
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ var ConnectorRegistry = _ConnectorRegistry;
60
60
  // src/connectors/postgres/index.ts
61
61
  var { Pool } = pg;
62
62
  var PostgresDSNParser = class {
63
- parse(dsn) {
63
+ async parse(dsn) {
64
64
  if (!this.isValidDSN(dsn)) {
65
65
  throw new Error(`Invalid PostgreSQL DSN: ${dsn}`);
66
66
  }
@@ -72,7 +72,7 @@ var PostgresDSNParser = class {
72
72
  database: url.pathname.substring(1),
73
73
  // Remove leading '/'
74
74
  user: url.username,
75
- password: url.password
75
+ password: url.password ? decodeURIComponent(url.password) : ""
76
76
  };
77
77
  url.searchParams.forEach((value, key) => {
78
78
  if (key === "sslmode") {
@@ -105,7 +105,7 @@ var PostgresConnector = class {
105
105
  }
106
106
  async connect(dsn) {
107
107
  try {
108
- const config = this.dsnParser.parse(dsn);
108
+ const config = await this.dsnParser.parse(dsn);
109
109
  this.pool = new Pool(config);
110
110
  const client = await this.pool.connect();
111
111
  console.error("Successfully connected to PostgreSQL database");
@@ -363,8 +363,9 @@ ConnectorRegistry.register(postgresConnector);
363
363
 
364
364
  // src/connectors/sqlserver/index.ts
365
365
  import sql from "mssql";
366
+ import { DefaultAzureCredential } from "@azure/identity";
366
367
  var SQLServerDSNParser = class {
367
- parse(dsn) {
368
+ async parse(dsn) {
368
369
  if (!this.isValidDSN(dsn)) {
369
370
  throw new Error("Invalid SQL Server DSN format. Expected: sqlserver://username:password@host:port/database");
370
371
  }
@@ -373,7 +374,7 @@ var SQLServerDSNParser = class {
373
374
  const port = url.port ? parseInt(url.port, 10) : 1433;
374
375
  const database = url.pathname.substring(1);
375
376
  const user = url.username;
376
- const password = url.password;
377
+ const password = url.password ? decodeURIComponent(url.password) : "";
377
378
  const options = {};
378
379
  for (const [key, value] of url.searchParams.entries()) {
379
380
  if (key === "encrypt") {
@@ -384,9 +385,11 @@ var SQLServerDSNParser = class {
384
385
  options.connectTimeout = parseInt(value, 10);
385
386
  } else if (key === "requestTimeout") {
386
387
  options.requestTimeout = parseInt(value, 10);
388
+ } else if (key === "authentication") {
389
+ options.authentication = value;
387
390
  }
388
391
  }
389
- return {
392
+ const config = {
390
393
  user,
391
394
  password,
392
395
  server: host,
@@ -401,6 +404,22 @@ var SQLServerDSNParser = class {
401
404
  requestTimeout: options.requestTimeout ?? 15e3
402
405
  }
403
406
  };
407
+ if (options.authentication === "azure-active-directory-access-token") {
408
+ try {
409
+ const credential = new DefaultAzureCredential();
410
+ const token = await credential.getToken("https://database.windows.net/");
411
+ config.authentication = {
412
+ type: "azure-active-directory-access-token",
413
+ options: {
414
+ token: token.token
415
+ }
416
+ };
417
+ } catch (error) {
418
+ const errorMessage = error instanceof Error ? error.message : String(error);
419
+ throw new Error(`Failed to get Azure AD token: ${errorMessage}`);
420
+ }
421
+ }
422
+ return config;
404
423
  }
405
424
  getSampleDSN() {
406
425
  return "sqlserver://username:password@localhost:1433/database?encrypt=true";
@@ -422,7 +441,7 @@ var SQLServerConnector = class {
422
441
  }
423
442
  async connect(dsn) {
424
443
  try {
425
- this.config = this.dsnParser.parse(dsn);
444
+ this.config = await this.dsnParser.parse(dsn);
426
445
  if (!this.config.options) {
427
446
  this.config.options = {};
428
447
  }
@@ -443,9 +462,9 @@ var SQLServerConnector = class {
443
462
  }
444
463
  try {
445
464
  const result = await this.connection.request().query(`
446
- SELECT SCHEMA_NAME
447
- FROM INFORMATION_SCHEMA.SCHEMATA
448
- ORDER BY SCHEMA_NAME
465
+ SELECT SCHEMA_NAME
466
+ FROM INFORMATION_SCHEMA.SCHEMATA
467
+ ORDER BY SCHEMA_NAME
449
468
  `);
450
469
  return result.recordset.map((row) => row.SCHEMA_NAME);
451
470
  } catch (error) {
@@ -460,10 +479,10 @@ var SQLServerConnector = class {
460
479
  const schemaToUse = schema || "dbo";
461
480
  const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
462
481
  const query = `
463
- SELECT TABLE_NAME
464
- FROM INFORMATION_SCHEMA.TABLES
465
- WHERE TABLE_SCHEMA = @schema
466
- ORDER BY TABLE_NAME
482
+ SELECT TABLE_NAME
483
+ FROM INFORMATION_SCHEMA.TABLES
484
+ WHERE TABLE_SCHEMA = @schema
485
+ ORDER BY TABLE_NAME
467
486
  `;
468
487
  const result = await request.query(query);
469
488
  return result.recordset.map((row) => row.TABLE_NAME);
@@ -479,10 +498,10 @@ var SQLServerConnector = class {
479
498
  const schemaToUse = schema || "dbo";
480
499
  const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
481
500
  const query = `
482
- SELECT COUNT(*) as count
483
- FROM INFORMATION_SCHEMA.TABLES
484
- WHERE TABLE_NAME = @tableName
485
- AND TABLE_SCHEMA = @schema
501
+ SELECT COUNT(*) as count
502
+ FROM INFORMATION_SCHEMA.TABLES
503
+ WHERE TABLE_NAME = @tableName
504
+ AND TABLE_SCHEMA = @schema
486
505
  `;
487
506
  const result = await request.query(query);
488
507
  return result.recordset[0].count > 0;
@@ -498,28 +517,24 @@ var SQLServerConnector = class {
498
517
  const schemaToUse = schema || "dbo";
499
518
  const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
500
519
  const query = `
501
- SELECT
502
- i.name AS index_name,
503
- i.is_unique,
504
- i.is_primary_key,
505
- c.name AS column_name,
506
- ic.key_ordinal
507
- FROM
508
- sys.indexes i
509
- INNER JOIN
510
- sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
511
- INNER JOIN
512
- sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
513
- INNER JOIN
514
- sys.tables t ON i.object_id = t.object_id
515
- INNER JOIN
516
- sys.schemas s ON t.schema_id = s.schema_id
517
- WHERE
518
- t.name = @tableName
519
- AND s.name = @schema
520
- ORDER BY
521
- i.name,
522
- ic.key_ordinal
520
+ SELECT i.name AS index_name,
521
+ i.is_unique,
522
+ i.is_primary_key,
523
+ c.name AS column_name,
524
+ ic.key_ordinal
525
+ FROM sys.indexes i
526
+ INNER JOIN
527
+ sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
528
+ INNER JOIN
529
+ sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
530
+ INNER JOIN
531
+ sys.tables t ON i.object_id = t.object_id
532
+ INNER JOIN
533
+ sys.schemas s ON t.schema_id = s.schema_id
534
+ WHERE t.name = @tableName
535
+ AND s.name = @schema
536
+ ORDER BY i.name,
537
+ ic.key_ordinal
523
538
  `;
524
539
  const result = await request.query(query);
525
540
  const indexMap = /* @__PURE__ */ new Map();
@@ -560,15 +575,14 @@ var SQLServerConnector = class {
560
575
  const schemaToUse = schema || "dbo";
561
576
  const request = this.connection.request().input("tableName", sql.VarChar, tableName).input("schema", sql.VarChar, schemaToUse);
562
577
  const query = `
563
- SELECT
564
- COLUMN_NAME as column_name,
565
- DATA_TYPE as data_type,
566
- IS_NULLABLE as is_nullable,
567
- COLUMN_DEFAULT as column_default
568
- FROM INFORMATION_SCHEMA.COLUMNS
569
- WHERE TABLE_NAME = @tableName
570
- AND TABLE_SCHEMA = @schema
571
- ORDER BY ORDINAL_POSITION
578
+ SELECT COLUMN_NAME as column_name,
579
+ DATA_TYPE as data_type,
580
+ IS_NULLABLE as is_nullable,
581
+ COLUMN_DEFAULT as column_default
582
+ FROM INFORMATION_SCHEMA.COLUMNS
583
+ WHERE TABLE_NAME = @tableName
584
+ AND TABLE_SCHEMA = @schema
585
+ ORDER BY ORDINAL_POSITION
572
586
  `;
573
587
  const result = await request.query(query);
574
588
  return result.recordset;
@@ -584,11 +598,11 @@ var SQLServerConnector = class {
584
598
  const schemaToUse = schema || "dbo";
585
599
  const request = this.connection.request().input("schema", sql.VarChar, schemaToUse);
586
600
  const query = `
587
- SELECT ROUTINE_NAME
588
- FROM INFORMATION_SCHEMA.ROUTINES
589
- WHERE ROUTINE_SCHEMA = @schema
590
- AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')
591
- ORDER BY ROUTINE_NAME
601
+ SELECT ROUTINE_NAME
602
+ FROM INFORMATION_SCHEMA.ROUTINES
603
+ WHERE ROUTINE_SCHEMA = @schema
604
+ AND (ROUTINE_TYPE = 'PROCEDURE' OR ROUTINE_TYPE = 'FUNCTION')
605
+ ORDER BY ROUTINE_NAME
592
606
  `;
593
607
  const result = await request.query(query);
594
608
  return result.recordset.map((row) => row.ROUTINE_NAME);
@@ -604,13 +618,12 @@ var SQLServerConnector = class {
604
618
  const schemaToUse = schema || "dbo";
605
619
  const request = this.connection.request().input("procedureName", sql.VarChar, procedureName).input("schema", sql.VarChar, schemaToUse);
606
620
  const routineQuery = `
607
- SELECT
608
- ROUTINE_NAME as procedure_name,
609
- ROUTINE_TYPE,
610
- DATA_TYPE as return_data_type
611
- FROM INFORMATION_SCHEMA.ROUTINES
612
- WHERE ROUTINE_NAME = @procedureName
613
- AND ROUTINE_SCHEMA = @schema
621
+ SELECT ROUTINE_NAME as procedure_name,
622
+ ROUTINE_TYPE,
623
+ DATA_TYPE as return_data_type
624
+ FROM INFORMATION_SCHEMA.ROUTINES
625
+ WHERE ROUTINE_NAME = @procedureName
626
+ AND ROUTINE_SCHEMA = @schema
614
627
  `;
615
628
  const routineResult = await request.query(routineQuery);
616
629
  if (routineResult.recordset.length === 0) {
@@ -618,16 +631,15 @@ var SQLServerConnector = class {
618
631
  }
619
632
  const routine = routineResult.recordset[0];
620
633
  const parameterQuery = `
621
- SELECT
622
- PARAMETER_NAME,
623
- PARAMETER_MODE,
624
- DATA_TYPE,
625
- CHARACTER_MAXIMUM_LENGTH,
626
- ORDINAL_POSITION
627
- FROM INFORMATION_SCHEMA.PARAMETERS
628
- WHERE SPECIFIC_NAME = @procedureName
629
- AND SPECIFIC_SCHEMA = @schema
630
- ORDER BY ORDINAL_POSITION
634
+ SELECT PARAMETER_NAME,
635
+ PARAMETER_MODE,
636
+ DATA_TYPE,
637
+ CHARACTER_MAXIMUM_LENGTH,
638
+ ORDINAL_POSITION
639
+ FROM INFORMATION_SCHEMA.PARAMETERS
640
+ WHERE SPECIFIC_NAME = @procedureName
641
+ AND SPECIFIC_SCHEMA = @schema
642
+ ORDER BY ORDINAL_POSITION
631
643
  `;
632
644
  const parameterResult = await request.query(parameterQuery);
633
645
  let parameterList = "";
@@ -638,12 +650,12 @@ var SQLServerConnector = class {
638
650
  }).join(", ");
639
651
  }
640
652
  const definitionQuery = `
641
- SELECT definition
642
- FROM sys.sql_modules sm
643
- JOIN sys.objects o ON sm.object_id = o.object_id
644
- JOIN sys.schemas s ON o.schema_id = s.schema_id
645
- WHERE o.name = @procedureName
646
- AND s.name = @schema
653
+ SELECT definition
654
+ FROM sys.sql_modules sm
655
+ JOIN sys.objects o ON sm.object_id = o.object_id
656
+ JOIN sys.schemas s ON o.schema_id = s.schema_id
657
+ WHERE o.name = @procedureName
658
+ AND s.name = @schema
647
659
  `;
648
660
  const definitionResult = await request.query(definitionQuery);
649
661
  let definition = void 0;
@@ -701,7 +713,7 @@ ConnectorRegistry.register(sqlServerConnector);
701
713
  // src/connectors/sqlite/index.ts
702
714
  import Database from "better-sqlite3";
703
715
  var SQLiteDSNParser = class {
704
- parse(dsn) {
716
+ async parse(dsn) {
705
717
  if (!this.isValidDSN(dsn)) {
706
718
  throw new Error(`Invalid SQLite DSN: ${dsn}`);
707
719
  }
@@ -744,7 +756,7 @@ var SQLiteConnector = class {
744
756
  }
745
757
  // Default to in-memory database
746
758
  async connect(dsn, initScript) {
747
- const config = this.dsnParser.parse(dsn);
759
+ const config = await this.dsnParser.parse(dsn);
748
760
  this.dbPath = config.dbPath;
749
761
  try {
750
762
  this.db = new Database(this.dbPath);
@@ -908,7 +920,7 @@ ConnectorRegistry.register(sqliteConnector);
908
920
  // src/connectors/mysql/index.ts
909
921
  import mysql from "mysql2/promise";
910
922
  var MySQLDSNParser = class {
911
- parse(dsn) {
923
+ async parse(dsn) {
912
924
  if (!this.isValidDSN(dsn)) {
913
925
  throw new Error(`Invalid MySQL DSN: ${dsn}`);
914
926
  }
@@ -920,7 +932,7 @@ var MySQLDSNParser = class {
920
932
  database: url.pathname.substring(1),
921
933
  // Remove leading '/'
922
934
  user: url.username,
923
- password: url.password
935
+ password: url.password ? decodeURIComponent(url.password) : ""
924
936
  };
925
937
  url.searchParams.forEach((value, key) => {
926
938
  if (key === "ssl") {
@@ -953,7 +965,7 @@ var MySQLConnector = class {
953
965
  }
954
966
  async connect(dsn) {
955
967
  try {
956
- const config = this.dsnParser.parse(dsn);
968
+ const config = await this.dsnParser.parse(dsn);
957
969
  this.pool = mysql.createPool(config);
958
970
  const [rows] = await this.pool.query("SELECT 1");
959
971
  console.error("Successfully connected to MySQL database");
@@ -1247,6 +1259,348 @@ var MySQLConnector = class {
1247
1259
  var mysqlConnector = new MySQLConnector();
1248
1260
  ConnectorRegistry.register(mysqlConnector);
1249
1261
 
1262
+ // src/connectors/mariadb/index.ts
1263
+ import mariadb from "mariadb";
1264
+ var MariadbDSNParser = class {
1265
+ async parse(dsn) {
1266
+ if (!this.isValidDSN(dsn)) {
1267
+ throw new Error(`Invalid MariaDB DSN: ${dsn}`);
1268
+ }
1269
+ try {
1270
+ const url = new URL(dsn);
1271
+ const config = {
1272
+ host: url.hostname,
1273
+ port: url.port ? parseInt(url.port) : 3306,
1274
+ database: url.pathname.substring(1),
1275
+ user: url.username,
1276
+ password: decodeURIComponent(url.password)
1277
+ };
1278
+ url.searchParams.forEach((value, key) => {
1279
+ if (key === "ssl") {
1280
+ config.ssl = value === "true" ? {} : void 0;
1281
+ }
1282
+ });
1283
+ return config;
1284
+ } catch (error) {
1285
+ throw new Error(`Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`);
1286
+ }
1287
+ }
1288
+ getSampleDSN() {
1289
+ return "mariadb://root:password@localhost:3306/db";
1290
+ }
1291
+ isValidDSN(dsn) {
1292
+ try {
1293
+ const url = new URL(dsn);
1294
+ return url.protocol === "mariadb:";
1295
+ } catch (error) {
1296
+ return false;
1297
+ }
1298
+ }
1299
+ };
1300
+ var MariaDBConnector = class {
1301
+ constructor() {
1302
+ this.id = "mariadb";
1303
+ this.name = "MariaDB";
1304
+ this.dsnParser = new MariadbDSNParser();
1305
+ this.pool = null;
1306
+ }
1307
+ async connect(dsn) {
1308
+ try {
1309
+ const config = await this.dsnParser.parse(dsn);
1310
+ this.pool = mariadb.createPool(config);
1311
+ console.error("Testing connection to MariaDB...");
1312
+ const [rows] = await this.pool.query("SELECT 1");
1313
+ console.error("Successfully connected to MariaDB database");
1314
+ } catch (err) {
1315
+ console.error("Failed to connect to MariaDB database:", err);
1316
+ throw err;
1317
+ }
1318
+ }
1319
+ async disconnect() {
1320
+ if (this.pool) {
1321
+ await this.pool.end();
1322
+ this.pool = null;
1323
+ }
1324
+ }
1325
+ async getSchemas() {
1326
+ if (!this.pool) {
1327
+ throw new Error("Not connected to database");
1328
+ }
1329
+ try {
1330
+ const [rows] = await this.pool.query(`
1331
+ SELECT SCHEMA_NAME
1332
+ FROM INFORMATION_SCHEMA.SCHEMATA
1333
+ ORDER BY SCHEMA_NAME
1334
+ `);
1335
+ return rows.map((row) => row.SCHEMA_NAME);
1336
+ } catch (error) {
1337
+ console.error("Error getting schemas:", error);
1338
+ throw error;
1339
+ }
1340
+ }
1341
+ async getTables(schema) {
1342
+ if (!this.pool) {
1343
+ throw new Error("Not connected to database");
1344
+ }
1345
+ try {
1346
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1347
+ const queryParams = schema ? [schema] : [];
1348
+ const [rows] = await this.pool.query(`
1349
+ SELECT TABLE_NAME
1350
+ FROM INFORMATION_SCHEMA.TABLES
1351
+ ${schemaClause}
1352
+ ORDER BY TABLE_NAME
1353
+ `, queryParams);
1354
+ return rows.map((row) => row.TABLE_NAME);
1355
+ } catch (error) {
1356
+ console.error("Error getting tables:", error);
1357
+ throw error;
1358
+ }
1359
+ }
1360
+ async tableExists(tableName, schema) {
1361
+ if (!this.pool) {
1362
+ throw new Error("Not connected to database");
1363
+ }
1364
+ try {
1365
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1366
+ const queryParams = schema ? [schema, tableName] : [tableName];
1367
+ const [rows] = await this.pool.query(`
1368
+ SELECT COUNT(*) AS COUNT
1369
+ FROM INFORMATION_SCHEMA.TABLES
1370
+ ${schemaClause}
1371
+ AND TABLE_NAME = ?
1372
+ `, queryParams);
1373
+ return rows[0].COUNT > 0;
1374
+ } catch (error) {
1375
+ console.error("Error checking if table exists:", error);
1376
+ throw error;
1377
+ }
1378
+ }
1379
+ async getTableIndexes(tableName, schema) {
1380
+ if (!this.pool) {
1381
+ throw new Error("Not connected to database");
1382
+ }
1383
+ try {
1384
+ const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
1385
+ const queryParams = schema ? [schema, tableName] : [tableName];
1386
+ const [indexRows] = await this.pool.query(`
1387
+ SELECT
1388
+ INDEX_NAME,
1389
+ COLUMN_NAME,
1390
+ NON_UNIQUE,
1391
+ SEQ_IN_INDEX
1392
+ FROM
1393
+ INFORMATION_SCHEMA.STATISTICS
1394
+ WHERE
1395
+ ${schemaClause}
1396
+ AND TABLE_NAME = ?
1397
+ ORDER BY
1398
+ INDEX_NAME,
1399
+ SEQ_IN_INDEX
1400
+ `, queryParams);
1401
+ const indexMap = /* @__PURE__ */ new Map();
1402
+ for (const row of indexRows) {
1403
+ const indexName = row.INDEX_NAME;
1404
+ const columnName = row.COLUMN_NAME;
1405
+ const isUnique = row.NON_UNIQUE === 0;
1406
+ const isPrimary = indexName === "PRIMARY";
1407
+ if (!indexMap.has(indexName)) {
1408
+ indexMap.set(indexName, {
1409
+ columns: [],
1410
+ is_unique: isUnique,
1411
+ is_primary: isPrimary
1412
+ });
1413
+ }
1414
+ const indexInfo = indexMap.get(indexName);
1415
+ indexInfo.columns.push(columnName);
1416
+ }
1417
+ const results = [];
1418
+ indexMap.forEach((indexInfo, indexName) => {
1419
+ results.push({
1420
+ index_name: indexName,
1421
+ column_names: indexInfo.columns,
1422
+ is_unique: indexInfo.is_unique,
1423
+ is_primary: indexInfo.is_primary
1424
+ });
1425
+ });
1426
+ return results;
1427
+ } catch (error) {
1428
+ console.error("Error getting table indexes:", error);
1429
+ throw error;
1430
+ }
1431
+ }
1432
+ async getTableSchema(tableName, schema) {
1433
+ if (!this.pool) {
1434
+ throw new Error("Not connected to database");
1435
+ }
1436
+ try {
1437
+ const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1438
+ const queryParams = schema ? [schema, tableName] : [tableName];
1439
+ const [rows] = await this.pool.query(`
1440
+ SELECT
1441
+ COLUMN_NAME as column_name,
1442
+ DATA_TYPE as data_type,
1443
+ IS_NULLABLE as is_nullable,
1444
+ COLUMN_DEFAULT as column_default
1445
+ FROM INFORMATION_SCHEMA.COLUMNS
1446
+ ${schemaClause}
1447
+ AND TABLE_NAME = ?
1448
+ ORDER BY ORDINAL_POSITION
1449
+ `, queryParams);
1450
+ return rows;
1451
+ } catch (error) {
1452
+ console.error("Error getting table schema:", error);
1453
+ throw error;
1454
+ }
1455
+ }
1456
+ async getStoredProcedures(schema) {
1457
+ if (!this.pool) {
1458
+ throw new Error("Not connected to database");
1459
+ }
1460
+ try {
1461
+ const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
1462
+ const queryParams = schema ? [schema] : [];
1463
+ const [rows] = await this.pool.query(`
1464
+ SELECT ROUTINE_NAME
1465
+ FROM INFORMATION_SCHEMA.ROUTINES
1466
+ ${schemaClause}
1467
+ ORDER BY ROUTINE_NAME
1468
+ `, queryParams);
1469
+ return rows.map((row) => row.ROUTINE_NAME);
1470
+ } catch (error) {
1471
+ console.error("Error getting stored procedures:", error);
1472
+ throw error;
1473
+ }
1474
+ }
1475
+ async getStoredProcedureDetail(procedureName, schema) {
1476
+ if (!this.pool) {
1477
+ throw new Error("Not connected to database");
1478
+ }
1479
+ try {
1480
+ const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
1481
+ const queryParams = schema ? [schema, procedureName] : [procedureName];
1482
+ const [rows] = await this.pool.query(`
1483
+ SELECT
1484
+ r.ROUTINE_NAME AS procedure_name,
1485
+ CASE
1486
+ WHEN r.ROUTINE_TYPE = 'PROCEDURE' THEN 'procedure'
1487
+ ELSE 'function'
1488
+ END AS procedure_type,
1489
+ LOWER(r.ROUTINE_TYPE) AS routine_type,
1490
+ r.ROUTINE_DEFINITION,
1491
+ r.DTD_IDENTIFIER AS return_type,
1492
+ (
1493
+ SELECT GROUP_CONCAT(
1494
+ CONCAT(p.PARAMETER_NAME, ' ', p.PARAMETER_MODE, ' ', p.DATA_TYPE)
1495
+ ORDER BY p.ORDINAL_POSITION
1496
+ SEPARATOR ', '
1497
+ )
1498
+ FROM INFORMATION_SCHEMA.PARAMETERS p
1499
+ WHERE p.SPECIFIC_SCHEMA = r.ROUTINE_SCHEMA
1500
+ AND p.SPECIFIC_NAME = r.ROUTINE_NAME
1501
+ AND p.PARAMETER_NAME IS NOT NULL
1502
+ ) AS parameter_list
1503
+ FROM INFORMATION_SCHEMA.ROUTINES r
1504
+ ${schemaClause}
1505
+ AND r.ROUTINE_NAME = ?
1506
+ `, queryParams);
1507
+ if (rows.length === 0) {
1508
+ const schemaName = schema || "current schema";
1509
+ throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
1510
+ }
1511
+ const procedure = rows[0];
1512
+ let definition = procedure.ROUTINE_DEFINITION;
1513
+ try {
1514
+ const schemaValue = schema || await this.getCurrentSchema();
1515
+ if (procedure.procedure_type === "procedure") {
1516
+ try {
1517
+ const [defRows] = await this.pool.query(`
1518
+ SHOW CREATE PROCEDURE ${schemaValue}.${procedureName}
1519
+ `);
1520
+ if (defRows && defRows.length > 0) {
1521
+ definition = defRows[0]["Create Procedure"];
1522
+ }
1523
+ } catch (err) {
1524
+ console.error(`Error getting procedure definition with SHOW CREATE: ${err}`);
1525
+ }
1526
+ } else {
1527
+ try {
1528
+ const [defRows] = await this.pool.query(`
1529
+ SHOW CREATE FUNCTION ${schemaValue}.${procedureName}
1530
+ `);
1531
+ if (defRows && defRows.length > 0) {
1532
+ definition = defRows[0]["Create Function"];
1533
+ }
1534
+ } catch (innerErr) {
1535
+ console.error(`Error getting function definition with SHOW CREATE: ${innerErr}`);
1536
+ }
1537
+ }
1538
+ if (!definition) {
1539
+ const [bodyRows] = await this.pool.query(`
1540
+ SELECT ROUTINE_DEFINITION, ROUTINE_BODY
1541
+ FROM INFORMATION_SCHEMA.ROUTINES
1542
+ WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
1543
+ `, [schemaValue, procedureName]);
1544
+ if (bodyRows && bodyRows.length > 0) {
1545
+ if (bodyRows[0].ROUTINE_DEFINITION) {
1546
+ definition = bodyRows[0].ROUTINE_DEFINITION;
1547
+ } else if (bodyRows[0].ROUTINE_BODY) {
1548
+ definition = bodyRows[0].ROUTINE_BODY;
1549
+ }
1550
+ }
1551
+ }
1552
+ } catch (error) {
1553
+ console.error(`Error getting procedure/function details: ${error}`);
1554
+ }
1555
+ return {
1556
+ procedure_name: procedure.procedure_name,
1557
+ procedure_type: procedure.procedure_type,
1558
+ language: "sql",
1559
+ // MariaDB procedures are generally in SQL
1560
+ parameter_list: procedure.parameter_list || "",
1561
+ return_type: procedure.routine_type === "function" ? procedure.return_type : void 0,
1562
+ definition: definition || void 0
1563
+ };
1564
+ } catch (error) {
1565
+ console.error("Error getting stored procedure detail:", error);
1566
+ throw error;
1567
+ }
1568
+ }
1569
+ // Helper method to get current schema (database) name
1570
+ async getCurrentSchema() {
1571
+ const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
1572
+ return rows[0].DB;
1573
+ }
1574
+ async executeQuery(query) {
1575
+ if (!this.pool) {
1576
+ throw new Error("Not connected to database");
1577
+ }
1578
+ const safetyCheck = this.validateQuery(query);
1579
+ if (!safetyCheck.isValid) {
1580
+ throw new Error(safetyCheck.message || "Query validation failed");
1581
+ }
1582
+ try {
1583
+ const [rows, fields] = await this.pool.query(query);
1584
+ return { rows, fields };
1585
+ } catch (error) {
1586
+ console.error("Error executing query:", error);
1587
+ throw error;
1588
+ }
1589
+ }
1590
+ validateQuery(query) {
1591
+ const normalizedQuery = query.trim().toLowerCase();
1592
+ if (!normalizedQuery.startsWith("select")) {
1593
+ return {
1594
+ isValid: false,
1595
+ message: "Only SELECT queries are allowed for security reasons."
1596
+ };
1597
+ }
1598
+ return { isValid: true };
1599
+ }
1600
+ };
1601
+ var mariadbConnector = new MariaDBConnector();
1602
+ ConnectorRegistry.register(mariadbConnector);
1603
+
1250
1604
  // src/server.ts
1251
1605
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
1252
1606
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
@@ -1437,6 +1791,17 @@ function resolvePort() {
1437
1791
  }
1438
1792
  return { port: 8080, source: "default" };
1439
1793
  }
1794
+ function redactDSN(dsn) {
1795
+ try {
1796
+ const url = new URL(dsn);
1797
+ if (url.password) {
1798
+ url.password = "*******";
1799
+ }
1800
+ return url.toString();
1801
+ } catch (error) {
1802
+ return dsn.replace(/\/\/([^:]+):([^@]+)@/, "//$1:***@");
1803
+ }
1804
+ }
1440
1805
 
1441
1806
  // src/config/demo-loader.ts
1442
1807
  import fs2 from "fs";
@@ -1875,11 +2240,13 @@ async function listConnectorsToolHandler(_args, _extra) {
1875
2240
  function registerTools(server) {
1876
2241
  server.tool(
1877
2242
  "run_query",
2243
+ "Run a SQL query on the current database",
1878
2244
  runQuerySchema,
1879
2245
  runQueryToolHandler
1880
2246
  );
1881
2247
  server.tool(
1882
2248
  "list_connectors",
2249
+ "List all available database connectors",
1883
2250
  {},
1884
2251
  listConnectorsToolHandler
1885
2252
  );
@@ -2336,7 +2703,7 @@ See documentation for more details on configuring database connections.
2336
2703
  registerTools(server);
2337
2704
  registerPrompts(server);
2338
2705
  const connectorManager = new ConnectorManager();
2339
- console.error(`Connecting with DSN: ${dsnData.dsn}`);
2706
+ console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
2340
2707
  console.error(`DSN source: ${dsnData.source}`);
2341
2708
  if (dsnData.isDemo) {
2342
2709
  console.error("Running in demo mode with sample employee database");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Universal Database MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -16,11 +16,13 @@
16
16
  "author": "",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
+ "@azure/identity": "^4.8.0",
19
20
  "@modelcontextprotocol/sdk": "^1.6.1",
20
21
  "better-sqlite3": "^11.9.0",
21
22
  "dotenv": "^16.4.7",
22
23
  "express": "^4.18.2",
23
24
  "mssql": "^11.0.1",
25
+ "mariadb": "^3.4.0",
24
26
  "mysql2": "^3.13.0",
25
27
  "pg": "^8.13.3",
26
28
  "zod": "^3.24.2"