@bytebase/dbhub 0.2.2 → 0.3.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 +43 -27
- package/dist/index.js +506 -138
- 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.
|
|
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
|
-
| | | +--->+
|
|
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
|
|
40
|
-
|
|
|
41
|
-
| schemas
|
|
42
|
-
| tables_in_schema
|
|
43
|
-
| table_structure_in_schema
|
|
44
|
-
| indexes_in_table
|
|
45
|
-
| procedures_in_schema
|
|
46
|
-
| procedure_details_in_schema
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
+
npx @bytebase/dbhub
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
- **Environment file** (third priority):
|
|
@@ -181,14 +181,26 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
|
|
|
181
181
|
DSN=postgres://user:password@localhost:5432/dbname?sslmode=disable
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
> [!WARNING]
|
|
185
|
+
> When running in Docker, use `host.docker.internal` instead of `localhost` to connect to databases running on your host machine. For example: `mysql://user:password@host.docker.internal:3306/dbname`
|
|
186
|
+
|
|
184
187
|
DBHub supports the following database connection string formats:
|
|
185
188
|
|
|
186
|
-
| Database | DSN Format | Example
|
|
187
|
-
| ---------- | -------------------------------------------------------- |
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
189
|
+
| Database | DSN Format | Example |
|
|
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` |
|
|
193
|
+
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
|
|
194
|
+
| SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname` |
|
|
195
|
+
| SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db` 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).
|
|
192
204
|
|
|
193
205
|
### Transport
|
|
194
206
|
|
|
@@ -254,3 +266,7 @@ npx @modelcontextprotocol/inspector
|
|
|
254
266
|
```
|
|
255
267
|
|
|
256
268
|
Connect to the DBHub server `/sse` endpoint
|
|
269
|
+
|
|
270
|
+
## Star History
|
|
271
|
+
|
|
272
|
+
[](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
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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");
|
|
@@ -974,11 +986,11 @@ var MySQLConnector = class {
|
|
|
974
986
|
}
|
|
975
987
|
try {
|
|
976
988
|
const [rows] = await this.pool.query(`
|
|
977
|
-
SELECT
|
|
978
|
-
FROM
|
|
979
|
-
ORDER BY
|
|
989
|
+
SELECT SCHEMA_NAME
|
|
990
|
+
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
991
|
+
ORDER BY SCHEMA_NAME
|
|
980
992
|
`);
|
|
981
|
-
return rows.map((row) => row.
|
|
993
|
+
return rows.map((row) => row.SCHEMA_NAME);
|
|
982
994
|
} catch (error) {
|
|
983
995
|
console.error("Error getting schemas:", error);
|
|
984
996
|
throw error;
|
|
@@ -989,15 +1001,15 @@ var MySQLConnector = class {
|
|
|
989
1001
|
throw new Error("Not connected to database");
|
|
990
1002
|
}
|
|
991
1003
|
try {
|
|
992
|
-
const schemaClause = schema ? "WHERE
|
|
1004
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
993
1005
|
const queryParams = schema ? [schema] : [];
|
|
994
1006
|
const [rows] = await this.pool.query(`
|
|
995
|
-
SELECT
|
|
996
|
-
FROM
|
|
1007
|
+
SELECT TABLE_NAME
|
|
1008
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
997
1009
|
${schemaClause}
|
|
998
|
-
ORDER BY
|
|
1010
|
+
ORDER BY TABLE_NAME
|
|
999
1011
|
`, queryParams);
|
|
1000
|
-
return rows.map((row) => row.
|
|
1012
|
+
return rows.map((row) => row.TABLE_NAME);
|
|
1001
1013
|
} catch (error) {
|
|
1002
1014
|
console.error("Error getting tables:", error);
|
|
1003
1015
|
throw error;
|
|
@@ -1008,15 +1020,15 @@ var MySQLConnector = class {
|
|
|
1008
1020
|
throw new Error("Not connected to database");
|
|
1009
1021
|
}
|
|
1010
1022
|
try {
|
|
1011
|
-
const schemaClause = schema ? "WHERE
|
|
1023
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1012
1024
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1013
1025
|
const [rows] = await this.pool.query(`
|
|
1014
|
-
SELECT COUNT(*)
|
|
1015
|
-
FROM
|
|
1026
|
+
SELECT COUNT(*) AS COUNT
|
|
1027
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
1016
1028
|
${schemaClause}
|
|
1017
|
-
AND
|
|
1029
|
+
AND TABLE_NAME = ?
|
|
1018
1030
|
`, queryParams);
|
|
1019
|
-
return rows[0].
|
|
1031
|
+
return rows[0].COUNT > 0;
|
|
1020
1032
|
} catch (error) {
|
|
1021
1033
|
console.error("Error checking if table exists:", error);
|
|
1022
1034
|
throw error;
|
|
@@ -1036,7 +1048,7 @@ var MySQLConnector = class {
|
|
|
1036
1048
|
NON_UNIQUE,
|
|
1037
1049
|
SEQ_IN_INDEX
|
|
1038
1050
|
FROM
|
|
1039
|
-
|
|
1051
|
+
INFORMATION_SCHEMA.STATISTICS
|
|
1040
1052
|
WHERE
|
|
1041
1053
|
${schemaClause}
|
|
1042
1054
|
AND TABLE_NAME = ?
|
|
@@ -1080,18 +1092,18 @@ var MySQLConnector = class {
|
|
|
1080
1092
|
throw new Error("Not connected to database");
|
|
1081
1093
|
}
|
|
1082
1094
|
try {
|
|
1083
|
-
const schemaClause = schema ? "WHERE
|
|
1095
|
+
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1084
1096
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1085
1097
|
const [rows] = await this.pool.query(`
|
|
1086
1098
|
SELECT
|
|
1087
|
-
column_name,
|
|
1088
|
-
data_type,
|
|
1089
|
-
is_nullable,
|
|
1090
|
-
column_default
|
|
1091
|
-
FROM
|
|
1099
|
+
COLUMN_NAME as column_name,
|
|
1100
|
+
DATA_TYPE as data_type,
|
|
1101
|
+
IS_NULLABLE as is_nullable,
|
|
1102
|
+
COLUMN_DEFAULT as column_default
|
|
1103
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
1092
1104
|
${schemaClause}
|
|
1093
|
-
AND
|
|
1094
|
-
ORDER BY
|
|
1105
|
+
AND TABLE_NAME = ?
|
|
1106
|
+
ORDER BY ORDINAL_POSITION
|
|
1095
1107
|
`, queryParams);
|
|
1096
1108
|
return rows;
|
|
1097
1109
|
} catch (error) {
|
|
@@ -1104,15 +1116,15 @@ var MySQLConnector = class {
|
|
|
1104
1116
|
throw new Error("Not connected to database");
|
|
1105
1117
|
}
|
|
1106
1118
|
try {
|
|
1107
|
-
const schemaClause = schema ? "WHERE
|
|
1119
|
+
const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
|
|
1108
1120
|
const queryParams = schema ? [schema] : [];
|
|
1109
1121
|
const [rows] = await this.pool.query(`
|
|
1110
|
-
SELECT
|
|
1111
|
-
FROM
|
|
1122
|
+
SELECT ROUTINE_NAME
|
|
1123
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1112
1124
|
${schemaClause}
|
|
1113
|
-
ORDER BY
|
|
1125
|
+
ORDER BY ROUTINE_NAME
|
|
1114
1126
|
`, queryParams);
|
|
1115
|
-
return rows.map((row) => row.
|
|
1127
|
+
return rows.map((row) => row.ROUTINE_NAME);
|
|
1116
1128
|
} catch (error) {
|
|
1117
1129
|
console.error("Error getting stored procedures:", error);
|
|
1118
1130
|
throw error;
|
|
@@ -1123,39 +1135,39 @@ var MySQLConnector = class {
|
|
|
1123
1135
|
throw new Error("Not connected to database");
|
|
1124
1136
|
}
|
|
1125
1137
|
try {
|
|
1126
|
-
const schemaClause = schema ? "WHERE r.
|
|
1138
|
+
const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
|
|
1127
1139
|
const queryParams = schema ? [schema, procedureName] : [procedureName];
|
|
1128
1140
|
const [rows] = await this.pool.query(`
|
|
1129
1141
|
SELECT
|
|
1130
|
-
r.
|
|
1142
|
+
r.ROUTINE_NAME AS procedure_name,
|
|
1131
1143
|
CASE
|
|
1132
|
-
WHEN r.
|
|
1144
|
+
WHEN r.ROUTINE_TYPE = 'PROCEDURE' THEN 'procedure'
|
|
1133
1145
|
ELSE 'function'
|
|
1134
1146
|
END AS procedure_type,
|
|
1135
|
-
LOWER(r.
|
|
1136
|
-
r.
|
|
1137
|
-
r.
|
|
1147
|
+
LOWER(r.ROUTINE_TYPE) AS routine_type,
|
|
1148
|
+
r.ROUTINE_DEFINITION,
|
|
1149
|
+
r.DTD_IDENTIFIER AS return_type,
|
|
1138
1150
|
(
|
|
1139
1151
|
SELECT GROUP_CONCAT(
|
|
1140
|
-
CONCAT(p.
|
|
1141
|
-
ORDER BY p.
|
|
1152
|
+
CONCAT(p.PARAMETER_NAME, ' ', p.PARAMETER_MODE, ' ', p.DATA_TYPE)
|
|
1153
|
+
ORDER BY p.ORDINAL_POSITION
|
|
1142
1154
|
SEPARATOR ', '
|
|
1143
1155
|
)
|
|
1144
|
-
FROM
|
|
1145
|
-
WHERE p.
|
|
1146
|
-
AND p.
|
|
1147
|
-
AND p.
|
|
1156
|
+
FROM INFORMATION_SCHEMA.PARAMETERS p
|
|
1157
|
+
WHERE p.SPECIFIC_SCHEMA = r.ROUTINE_SCHEMA
|
|
1158
|
+
AND p.SPECIFIC_NAME = r.ROUTINE_NAME
|
|
1159
|
+
AND p.PARAMETER_NAME IS NOT NULL
|
|
1148
1160
|
) AS parameter_list
|
|
1149
|
-
FROM
|
|
1161
|
+
FROM INFORMATION_SCHEMA.ROUTINES r
|
|
1150
1162
|
${schemaClause}
|
|
1151
|
-
AND r.
|
|
1163
|
+
AND r.ROUTINE_NAME = ?
|
|
1152
1164
|
`, queryParams);
|
|
1153
1165
|
if (rows.length === 0) {
|
|
1154
1166
|
const schemaName = schema || "current schema";
|
|
1155
1167
|
throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
|
|
1156
1168
|
}
|
|
1157
1169
|
const procedure = rows[0];
|
|
1158
|
-
let definition = procedure.
|
|
1170
|
+
let definition = procedure.ROUTINE_DEFINITION;
|
|
1159
1171
|
try {
|
|
1160
1172
|
const schemaValue = schema || await this.getCurrentSchema();
|
|
1161
1173
|
if (procedure.procedure_type === "procedure") {
|
|
@@ -1183,15 +1195,15 @@ var MySQLConnector = class {
|
|
|
1183
1195
|
}
|
|
1184
1196
|
if (!definition) {
|
|
1185
1197
|
const [bodyRows] = await this.pool.query(`
|
|
1186
|
-
SELECT
|
|
1187
|
-
FROM
|
|
1188
|
-
WHERE
|
|
1198
|
+
SELECT ROUTINE_DEFINITION, ROUTINE_BODY
|
|
1199
|
+
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1200
|
+
WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
|
|
1189
1201
|
`, [schemaValue, procedureName]);
|
|
1190
1202
|
if (bodyRows && bodyRows.length > 0) {
|
|
1191
|
-
if (bodyRows[0].
|
|
1192
|
-
definition = bodyRows[0].
|
|
1193
|
-
} else if (bodyRows[0].
|
|
1194
|
-
definition = bodyRows[0].
|
|
1203
|
+
if (bodyRows[0].ROUTINE_DEFINITION) {
|
|
1204
|
+
definition = bodyRows[0].ROUTINE_DEFINITION;
|
|
1205
|
+
} else if (bodyRows[0].ROUTINE_BODY) {
|
|
1206
|
+
definition = bodyRows[0].ROUTINE_BODY;
|
|
1195
1207
|
}
|
|
1196
1208
|
}
|
|
1197
1209
|
}
|
|
@@ -1214,8 +1226,8 @@ var MySQLConnector = class {
|
|
|
1214
1226
|
}
|
|
1215
1227
|
// Helper method to get current schema (database) name
|
|
1216
1228
|
async getCurrentSchema() {
|
|
1217
|
-
const [rows] = await this.pool.query("SELECT DATABASE()
|
|
1218
|
-
return rows[0].
|
|
1229
|
+
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
1230
|
+
return rows[0].DB;
|
|
1219
1231
|
}
|
|
1220
1232
|
async executeQuery(query) {
|
|
1221
1233
|
if (!this.pool) {
|
|
@@ -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";
|
|
@@ -1595,7 +1960,7 @@ async function tablesResourceHandler(uri, variables, _extra) {
|
|
|
1595
1960
|
}
|
|
1596
1961
|
|
|
1597
1962
|
// src/resources/schema.ts
|
|
1598
|
-
async function
|
|
1963
|
+
async function tableStructureResourceHandler(uri, variables, _extra) {
|
|
1599
1964
|
const connector = ConnectorManager.getCurrentConnector();
|
|
1600
1965
|
const tableName = Array.isArray(variables.tableName) ? variables.tableName[0] : variables.tableName;
|
|
1601
1966
|
const schemaName = variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
@@ -1796,7 +2161,7 @@ function registerResources(server) {
|
|
|
1796
2161
|
server.resource(
|
|
1797
2162
|
"table_structure_in_schema",
|
|
1798
2163
|
new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}", { list: void 0 }),
|
|
1799
|
-
|
|
2164
|
+
tableStructureResourceHandler
|
|
1800
2165
|
);
|
|
1801
2166
|
server.resource(
|
|
1802
2167
|
"indexes_in_table",
|
|
@@ -1846,7 +2211,6 @@ async function runQueryToolHandler({ query }, _extra) {
|
|
|
1846
2211
|
|
|
1847
2212
|
// src/tools/list-connectors.ts
|
|
1848
2213
|
async function listConnectorsToolHandler(_args, _extra) {
|
|
1849
|
-
const connectors = ConnectorManager.getAvailableConnectors();
|
|
1850
2214
|
const samples = ConnectorRegistry.getAllSampleDSNs();
|
|
1851
2215
|
let activeConnectorId = null;
|
|
1852
2216
|
try {
|
|
@@ -2169,6 +2533,10 @@ ${possibleTableMatches.join("\n")}`
|
|
|
2169
2533
|
"EXPLANATION_ERROR"
|
|
2170
2534
|
);
|
|
2171
2535
|
}
|
|
2536
|
+
return formatPromptErrorResponse(
|
|
2537
|
+
`Unable to process request for schema: ${schema}, table: ${table}`,
|
|
2538
|
+
"UNHANDLED_REQUEST"
|
|
2539
|
+
);
|
|
2172
2540
|
}
|
|
2173
2541
|
function determineTablePurpose(tableName, columns) {
|
|
2174
2542
|
const lowerTableName = tableName.toLowerCase();
|
|
@@ -2333,7 +2701,7 @@ See documentation for more details on configuring database connections.
|
|
|
2333
2701
|
registerTools(server);
|
|
2334
2702
|
registerPrompts(server);
|
|
2335
2703
|
const connectorManager = new ConnectorManager();
|
|
2336
|
-
console.error(`Connecting with DSN: ${dsnData.dsn}`);
|
|
2704
|
+
console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
|
|
2337
2705
|
console.error(`DSN source: ${dsnData.source}`);
|
|
2338
2706
|
if (dsnData.isDemo) {
|
|
2339
2707
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
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"
|