@bytebase/dbhub 0.8.4 → 0.9.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 +21 -55
- package/dist/index.js +7 -319
- package/package.json +3 -4
- package/dist/resources/employee-sqlite/employee.sql +0 -117
- package/dist/resources/employee-sqlite/load_department.sql +0 -10
- package/dist/resources/employee-sqlite/load_dept_emp.sql +0 -1103
- package/dist/resources/employee-sqlite/load_dept_manager.sql +0 -17
- package/dist/resources/employee-sqlite/load_employee.sql +0 -1000
- package/dist/resources/employee-sqlite/load_salary1.sql +0 -9488
- package/dist/resources/employee-sqlite/load_title.sql +0 -1470
- package/dist/resources/employee-sqlite/object.sql +0 -74
- package/dist/resources/employee-sqlite/show_elapsed.sql +0 -4
- package/dist/resources/employee-sqlite/test_employee_md5.sql +0 -119
package/README.md
CHANGED
|
@@ -24,8 +24,6 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
|
|
|
24
24
|
| | | | | |
|
|
25
25
|
| Cursor +--->+ DBHub +--->+ SQL Server |
|
|
26
26
|
| | | | | |
|
|
27
|
-
| Other Clients +--->+ +--->+ SQLite |
|
|
28
|
-
| | | | | |
|
|
29
27
|
| | | +--->+ MySQL |
|
|
30
28
|
| | | | | |
|
|
31
29
|
| | | +--->+ MariaDB |
|
|
@@ -44,27 +42,27 @@ https://demo.dbhub.ai/message connects a [sample employee database](https://gith
|
|
|
44
42
|
|
|
45
43
|
### Database Resources
|
|
46
44
|
|
|
47
|
-
| Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server |
|
|
48
|
-
| --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: |
|
|
49
|
-
| schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ |
|
|
50
|
-
| tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ |
|
|
51
|
-
| table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ |
|
|
52
|
-
| indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ |
|
|
53
|
-
| procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ |
|
|
54
|
-
| procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ |
|
|
45
|
+
| Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server |
|
|
46
|
+
| --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: |
|
|
47
|
+
| schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ |
|
|
48
|
+
| tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ |
|
|
49
|
+
| table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ |
|
|
50
|
+
| indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ |
|
|
51
|
+
| procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ |
|
|
52
|
+
| procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ |
|
|
55
53
|
|
|
56
54
|
### Database Tools
|
|
57
55
|
|
|
58
|
-
| Tool | Command Name | Description | PostgreSQL | MySQL | MariaDB | SQL Server |
|
|
59
|
-
| ----------- | ------------- | ------------------------------------------------------------------- | :--------: | :---: | :-----: | :--------: |
|
|
60
|
-
| Execute SQL | `execute_sql` | Execute single or multiple SQL statements (separated by semicolons) | ✅ | ✅ | ✅ | ✅ |
|
|
56
|
+
| Tool | Command Name | Description | PostgreSQL | MySQL | MariaDB | SQL Server |
|
|
57
|
+
| ----------- | ------------- | ------------------------------------------------------------------- | :--------: | :---: | :-----: | :--------: |
|
|
58
|
+
| Execute SQL | `execute_sql` | Execute single or multiple SQL statements (separated by semicolons) | ✅ | ✅ | ✅ | ✅ |
|
|
61
59
|
|
|
62
60
|
### Prompt Capabilities
|
|
63
61
|
|
|
64
|
-
| Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server |
|
|
65
|
-
| ------------------- | -------------- | :--------: | :---: | :-----: | :--------: |
|
|
66
|
-
| Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ |
|
|
67
|
-
| Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ |
|
|
62
|
+
| Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server |
|
|
63
|
+
| ------------------- | -------------- | :--------: | :---: | :-----: | :--------: |
|
|
64
|
+
| Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ |
|
|
65
|
+
| Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ |
|
|
68
66
|
|
|
69
67
|
## Installation
|
|
70
68
|
|
|
@@ -81,16 +79,6 @@ docker run --rm --init \
|
|
|
81
79
|
--dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
82
80
|
```
|
|
83
81
|
|
|
84
|
-
```bash
|
|
85
|
-
# Demo mode with sample employee database
|
|
86
|
-
docker run --rm --init \
|
|
87
|
-
--name dbhub \
|
|
88
|
-
--publish 8080:8080 \
|
|
89
|
-
bytebase/dbhub \
|
|
90
|
-
--transport http \
|
|
91
|
-
--port 8080 \
|
|
92
|
-
--demo
|
|
93
|
-
```
|
|
94
82
|
|
|
95
83
|
|
|
96
84
|
### NPM
|
|
@@ -100,12 +88,6 @@ docker run --rm --init \
|
|
|
100
88
|
npx @bytebase/dbhub --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
101
89
|
```
|
|
102
90
|
|
|
103
|
-
```bash
|
|
104
|
-
# Demo mode with sample employee database
|
|
105
|
-
npx @bytebase/dbhub --transport http --port 8080 --demo
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
> Note: The demo mode includes a bundled SQLite sample "employee" database with tables for employees, departments, salaries, and more.
|
|
109
91
|
|
|
110
92
|
### Claude Desktop
|
|
111
93
|
|
|
@@ -142,10 +124,6 @@ npx @bytebase/dbhub --transport http --port 8080 --demo
|
|
|
142
124
|
"postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
143
125
|
]
|
|
144
126
|
},
|
|
145
|
-
"dbhub-demo": {
|
|
146
|
-
"command": "npx",
|
|
147
|
-
"args": ["-y", "@bytebase/dbhub", "--transport", "stdio", "--demo"]
|
|
148
|
-
}
|
|
149
127
|
}
|
|
150
128
|
}
|
|
151
129
|
```
|
|
@@ -171,7 +149,6 @@ You can specify the SSL mode using the `sslmode` parameter in your DSN string:
|
|
|
171
149
|
| MySQL | ✅ | ✅ | Certificate verification |
|
|
172
150
|
| MariaDB | ✅ | ✅ | Certificate verification |
|
|
173
151
|
| SQL Server | ✅ | ✅ | Certificate verification |
|
|
174
|
-
| SQLite | ❌ | ❌ | N/A (file-based) |
|
|
175
152
|
|
|
176
153
|
**SSL Mode Options:**
|
|
177
154
|
|
|
@@ -208,12 +185,6 @@ This provides an additional layer of security when connecting to production data
|
|
|
208
185
|
|
|
209
186
|
### Configure your database connection
|
|
210
187
|
|
|
211
|
-
You can use DBHub in demo mode with a sample employee database for testing:
|
|
212
|
-
|
|
213
|
-
```bash
|
|
214
|
-
npx @bytebase/dbhub --demo
|
|
215
|
-
```
|
|
216
|
-
|
|
217
188
|
> [!WARNING]
|
|
218
189
|
> If your user/password contains special characters, you need to escape them first. (e.g. `pass#word` should be escaped as `pass%23word`)
|
|
219
190
|
|
|
@@ -250,7 +221,6 @@ DBHub supports the following database connection string formats:
|
|
|
250
221
|
| MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname?sslmode=disable` |
|
|
251
222
|
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
|
|
252
223
|
| SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname?sslmode=disable` |
|
|
253
|
-
| SQLite | `sqlite:///[path/to/file]` or `sqlite:///:memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite:///:memory:` |
|
|
254
224
|
|
|
255
225
|
|
|
256
226
|
#### SQL Server
|
|
@@ -276,15 +246,13 @@ Extra query parameters:
|
|
|
276
246
|
|
|
277
247
|
### Command line options
|
|
278
248
|
|
|
279
|
-
| Option | Environment Variable | Description | Default
|
|
280
|
-
| --------- | -------------------- | ---------------------------------------------------------------- |
|
|
281
|
-
| dsn | `DSN` | Database connection string | Required
|
|
282
|
-
| transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio`
|
|
283
|
-
| port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080`
|
|
284
|
-
| readonly | `READONLY` | Restrict SQL execution to read-only operations | `false`
|
|
285
|
-
| demo | N/A | Run in demo mode with sample employee database | `false` |
|
|
249
|
+
| Option | Environment Variable | Description | Default |
|
|
250
|
+
| --------- | -------------------- | ---------------------------------------------------------------- | -------- |
|
|
251
|
+
| dsn | `DSN` | Database connection string | Required |
|
|
252
|
+
| transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
|
|
253
|
+
| port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
|
|
254
|
+
| readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
|
|
286
255
|
|
|
287
|
-
The demo mode uses an in-memory SQLite database loaded with the [sample employee database](https://github.com/bytebase/dbhub/tree/main/resources/employee-sqlite) that includes tables for employees, departments, titles, salaries, department employees, and department managers. The sample database includes SQL scripts for table creation, data loading, and testing.
|
|
288
256
|
|
|
289
257
|
## Development
|
|
290
258
|
|
|
@@ -342,8 +310,6 @@ pnpm test src/connectors/__tests__/mysql.integration.test.ts
|
|
|
342
310
|
pnpm test src/connectors/__tests__/mariadb.integration.test.ts
|
|
343
311
|
# Run only SQL Server integration tests
|
|
344
312
|
pnpm test src/connectors/__tests__/sqlserver.integration.test.ts
|
|
345
|
-
# Run only SQLite integration tests
|
|
346
|
-
pnpm test src/connectors/__tests__/sqlite.integration.test.ts
|
|
347
313
|
# Run JSON RPC integration tests
|
|
348
314
|
pnpm test src/__tests__/json-rpc-integration.test.ts
|
|
349
315
|
```
|
package/dist/index.js
CHANGED
|
@@ -159,9 +159,6 @@ function obfuscateDSNPassword(dsn) {
|
|
|
159
159
|
return dsn;
|
|
160
160
|
}
|
|
161
161
|
const protocol = protocolMatch[1];
|
|
162
|
-
if (protocol === "sqlite") {
|
|
163
|
-
return dsn;
|
|
164
|
-
}
|
|
165
162
|
const protocolPart = dsn.split("://")[1];
|
|
166
163
|
if (!protocolPart) {
|
|
167
164
|
return dsn;
|
|
@@ -879,246 +876,6 @@ var SQLServerConnector = class {
|
|
|
879
876
|
var sqlServerConnector = new SQLServerConnector();
|
|
880
877
|
ConnectorRegistry.register(sqlServerConnector);
|
|
881
878
|
|
|
882
|
-
// src/connectors/sqlite/index.ts
|
|
883
|
-
import Database from "better-sqlite3";
|
|
884
|
-
var SQLiteDSNParser = class {
|
|
885
|
-
async parse(dsn) {
|
|
886
|
-
if (!this.isValidDSN(dsn)) {
|
|
887
|
-
const obfuscatedDSN = obfuscateDSNPassword(dsn);
|
|
888
|
-
const expectedFormat = this.getSampleDSN();
|
|
889
|
-
throw new Error(
|
|
890
|
-
`Invalid SQLite DSN format.
|
|
891
|
-
Provided: ${obfuscatedDSN}
|
|
892
|
-
Expected: ${expectedFormat}`
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
try {
|
|
896
|
-
const url = new SafeURL(dsn);
|
|
897
|
-
let dbPath;
|
|
898
|
-
if (url.hostname === "" && url.pathname === "/:memory:") {
|
|
899
|
-
dbPath = ":memory:";
|
|
900
|
-
} else {
|
|
901
|
-
if (url.pathname.startsWith("//")) {
|
|
902
|
-
dbPath = url.pathname.substring(2);
|
|
903
|
-
} else {
|
|
904
|
-
dbPath = url.pathname;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
return { dbPath };
|
|
908
|
-
} catch (error) {
|
|
909
|
-
throw new Error(
|
|
910
|
-
`Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
getSampleDSN() {
|
|
915
|
-
return "sqlite:///path/to/database.db";
|
|
916
|
-
}
|
|
917
|
-
isValidDSN(dsn) {
|
|
918
|
-
try {
|
|
919
|
-
return dsn.startsWith("sqlite://");
|
|
920
|
-
} catch (error) {
|
|
921
|
-
return false;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
var SQLiteConnector = class {
|
|
926
|
-
constructor() {
|
|
927
|
-
this.id = "sqlite";
|
|
928
|
-
this.name = "SQLite";
|
|
929
|
-
this.dsnParser = new SQLiteDSNParser();
|
|
930
|
-
this.db = null;
|
|
931
|
-
this.dbPath = ":memory:";
|
|
932
|
-
}
|
|
933
|
-
// Default to in-memory database
|
|
934
|
-
async connect(dsn, initScript) {
|
|
935
|
-
const config = await this.dsnParser.parse(dsn);
|
|
936
|
-
this.dbPath = config.dbPath;
|
|
937
|
-
try {
|
|
938
|
-
this.db = new Database(this.dbPath);
|
|
939
|
-
console.error("Successfully connected to SQLite database");
|
|
940
|
-
if (initScript) {
|
|
941
|
-
this.db.exec(initScript);
|
|
942
|
-
console.error("Successfully initialized database with script");
|
|
943
|
-
}
|
|
944
|
-
} catch (error) {
|
|
945
|
-
console.error("Failed to connect to SQLite database:", error);
|
|
946
|
-
throw error;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
async disconnect() {
|
|
950
|
-
if (this.db) {
|
|
951
|
-
try {
|
|
952
|
-
this.db.close();
|
|
953
|
-
this.db = null;
|
|
954
|
-
} catch (error) {
|
|
955
|
-
throw error;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
return Promise.resolve();
|
|
959
|
-
}
|
|
960
|
-
async getSchemas() {
|
|
961
|
-
if (!this.db) {
|
|
962
|
-
throw new Error("Not connected to SQLite database");
|
|
963
|
-
}
|
|
964
|
-
return ["main"];
|
|
965
|
-
}
|
|
966
|
-
async getTables(schema) {
|
|
967
|
-
if (!this.db) {
|
|
968
|
-
throw new Error("Not connected to SQLite database");
|
|
969
|
-
}
|
|
970
|
-
try {
|
|
971
|
-
const rows = this.db.prepare(
|
|
972
|
-
`
|
|
973
|
-
SELECT name FROM sqlite_master
|
|
974
|
-
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
975
|
-
ORDER BY name
|
|
976
|
-
`
|
|
977
|
-
).all();
|
|
978
|
-
return rows.map((row) => row.name);
|
|
979
|
-
} catch (error) {
|
|
980
|
-
throw error;
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
async tableExists(tableName, schema) {
|
|
984
|
-
if (!this.db) {
|
|
985
|
-
throw new Error("Not connected to SQLite database");
|
|
986
|
-
}
|
|
987
|
-
try {
|
|
988
|
-
const row = this.db.prepare(
|
|
989
|
-
`
|
|
990
|
-
SELECT name FROM sqlite_master
|
|
991
|
-
WHERE type='table' AND name = ?
|
|
992
|
-
`
|
|
993
|
-
).get(tableName);
|
|
994
|
-
return !!row;
|
|
995
|
-
} catch (error) {
|
|
996
|
-
throw error;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
async getTableIndexes(tableName, schema) {
|
|
1000
|
-
if (!this.db) {
|
|
1001
|
-
throw new Error("Not connected to SQLite database");
|
|
1002
|
-
}
|
|
1003
|
-
try {
|
|
1004
|
-
const indexInfoRows = this.db.prepare(
|
|
1005
|
-
`
|
|
1006
|
-
SELECT
|
|
1007
|
-
name as index_name,
|
|
1008
|
-
0 as is_unique
|
|
1009
|
-
FROM sqlite_master
|
|
1010
|
-
WHERE type = 'index'
|
|
1011
|
-
AND tbl_name = ?
|
|
1012
|
-
`
|
|
1013
|
-
).all(tableName);
|
|
1014
|
-
const indexListRows = this.db.prepare(`PRAGMA index_list(${tableName})`).all();
|
|
1015
|
-
const indexUniqueMap = /* @__PURE__ */ new Map();
|
|
1016
|
-
for (const indexListRow of indexListRows) {
|
|
1017
|
-
indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
|
|
1018
|
-
}
|
|
1019
|
-
const tableInfo = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
1020
|
-
const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
|
|
1021
|
-
const results = [];
|
|
1022
|
-
for (const indexInfo of indexInfoRows) {
|
|
1023
|
-
const indexDetailRows = this.db.prepare(`PRAGMA index_info(${indexInfo.index_name})`).all();
|
|
1024
|
-
const columnNames = indexDetailRows.map((row) => row.name);
|
|
1025
|
-
results.push({
|
|
1026
|
-
index_name: indexInfo.index_name,
|
|
1027
|
-
column_names: columnNames,
|
|
1028
|
-
is_unique: indexUniqueMap.get(indexInfo.index_name) || false,
|
|
1029
|
-
is_primary: false
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
if (pkColumns.length > 0) {
|
|
1033
|
-
results.push({
|
|
1034
|
-
index_name: "PRIMARY",
|
|
1035
|
-
column_names: pkColumns,
|
|
1036
|
-
is_unique: true,
|
|
1037
|
-
is_primary: true
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
return results;
|
|
1041
|
-
} catch (error) {
|
|
1042
|
-
throw error;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
async getTableSchema(tableName, schema) {
|
|
1046
|
-
if (!this.db) {
|
|
1047
|
-
throw new Error("Not connected to SQLite database");
|
|
1048
|
-
}
|
|
1049
|
-
try {
|
|
1050
|
-
const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
1051
|
-
const columns = rows.map((row) => ({
|
|
1052
|
-
column_name: row.name,
|
|
1053
|
-
data_type: row.type,
|
|
1054
|
-
// In SQLite, primary key columns are automatically NOT NULL even if notnull=0
|
|
1055
|
-
is_nullable: row.notnull === 1 || row.pk > 0 ? "NO" : "YES",
|
|
1056
|
-
column_default: row.dflt_value
|
|
1057
|
-
}));
|
|
1058
|
-
return columns;
|
|
1059
|
-
} catch (error) {
|
|
1060
|
-
throw error;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
async getStoredProcedures(schema) {
|
|
1064
|
-
if (!this.db) {
|
|
1065
|
-
throw new Error("Not connected to SQLite database");
|
|
1066
|
-
}
|
|
1067
|
-
return [];
|
|
1068
|
-
}
|
|
1069
|
-
async getStoredProcedureDetail(procedureName, schema) {
|
|
1070
|
-
if (!this.db) {
|
|
1071
|
-
throw new Error("Not connected to SQLite database");
|
|
1072
|
-
}
|
|
1073
|
-
throw new Error(
|
|
1074
|
-
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
1075
|
-
);
|
|
1076
|
-
}
|
|
1077
|
-
async executeSQL(sql2) {
|
|
1078
|
-
if (!this.db) {
|
|
1079
|
-
throw new Error("Not connected to SQLite database");
|
|
1080
|
-
}
|
|
1081
|
-
try {
|
|
1082
|
-
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1083
|
-
if (statements.length === 1) {
|
|
1084
|
-
const trimmedStatement = statements[0].toLowerCase().trim();
|
|
1085
|
-
const isReadStatement = trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"));
|
|
1086
|
-
if (isReadStatement) {
|
|
1087
|
-
const rows = this.db.prepare(statements[0]).all();
|
|
1088
|
-
return { rows };
|
|
1089
|
-
} else {
|
|
1090
|
-
this.db.prepare(statements[0]).run();
|
|
1091
|
-
return { rows: [] };
|
|
1092
|
-
}
|
|
1093
|
-
} else {
|
|
1094
|
-
const readStatements = [];
|
|
1095
|
-
const writeStatements = [];
|
|
1096
|
-
for (const statement of statements) {
|
|
1097
|
-
const trimmedStatement = statement.toLowerCase().trim();
|
|
1098
|
-
if (trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"))) {
|
|
1099
|
-
readStatements.push(statement);
|
|
1100
|
-
} else {
|
|
1101
|
-
writeStatements.push(statement);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
if (writeStatements.length > 0) {
|
|
1105
|
-
this.db.exec(writeStatements.join("; "));
|
|
1106
|
-
}
|
|
1107
|
-
let allRows = [];
|
|
1108
|
-
for (const statement of readStatements) {
|
|
1109
|
-
const result = this.db.prepare(statement).all();
|
|
1110
|
-
allRows.push(...result);
|
|
1111
|
-
}
|
|
1112
|
-
return { rows: allRows };
|
|
1113
|
-
}
|
|
1114
|
-
} catch (error) {
|
|
1115
|
-
throw error;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
};
|
|
1119
|
-
var sqliteConnector = new SQLiteConnector();
|
|
1120
|
-
ConnectorRegistry.register(sqliteConnector);
|
|
1121
|
-
|
|
1122
879
|
// src/connectors/mysql/index.ts
|
|
1123
880
|
import mysql from "mysql2/promise";
|
|
1124
881
|
var MySQLDSNParser = class {
|
|
@@ -1880,9 +1637,9 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
|
|
|
1880
1637
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1881
1638
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1882
1639
|
import express from "express";
|
|
1883
|
-
import
|
|
1640
|
+
import path2 from "path";
|
|
1884
1641
|
import { readFileSync } from "fs";
|
|
1885
|
-
import { fileURLToPath as
|
|
1642
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1886
1643
|
|
|
1887
1644
|
// src/connectors/manager.ts
|
|
1888
1645
|
var managerInstance = null;
|
|
@@ -2016,10 +1773,6 @@ function loadEnvFiles() {
|
|
|
2016
1773
|
}
|
|
2017
1774
|
return null;
|
|
2018
1775
|
}
|
|
2019
|
-
function isDemoMode() {
|
|
2020
|
-
const args = parseCommandLineArgs();
|
|
2021
|
-
return args.demo === "true";
|
|
2022
|
-
}
|
|
2023
1776
|
function isReadOnlyMode() {
|
|
2024
1777
|
const args = parseCommandLineArgs();
|
|
2025
1778
|
if (args.readonly !== void 0) {
|
|
@@ -2032,13 +1785,6 @@ function isReadOnlyMode() {
|
|
|
2032
1785
|
}
|
|
2033
1786
|
function resolveDSN() {
|
|
2034
1787
|
const args = parseCommandLineArgs();
|
|
2035
|
-
if (isDemoMode()) {
|
|
2036
|
-
return {
|
|
2037
|
-
dsn: "sqlite:///:memory:",
|
|
2038
|
-
source: "demo mode",
|
|
2039
|
-
isDemo: true
|
|
2040
|
-
};
|
|
2041
|
-
}
|
|
2042
1788
|
if (args.dsn) {
|
|
2043
1789
|
return { dsn: args.dsn, source: "command line argument" };
|
|
2044
1790
|
}
|
|
@@ -2087,45 +1833,6 @@ function redactDSN(dsn) {
|
|
|
2087
1833
|
}
|
|
2088
1834
|
}
|
|
2089
1835
|
|
|
2090
|
-
// src/config/demo-loader.ts
|
|
2091
|
-
import fs2 from "fs";
|
|
2092
|
-
import path2 from "path";
|
|
2093
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2094
|
-
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2095
|
-
var __dirname2 = path2.dirname(__filename2);
|
|
2096
|
-
var DEMO_DATA_DIR;
|
|
2097
|
-
var projectRootPath = path2.join(__dirname2, "..", "..", "..");
|
|
2098
|
-
var projectResourcesPath = path2.join(projectRootPath, "resources", "employee-sqlite");
|
|
2099
|
-
var distPath = path2.join(__dirname2, "resources", "employee-sqlite");
|
|
2100
|
-
if (fs2.existsSync(projectResourcesPath)) {
|
|
2101
|
-
DEMO_DATA_DIR = projectResourcesPath;
|
|
2102
|
-
} else if (fs2.existsSync(distPath)) {
|
|
2103
|
-
DEMO_DATA_DIR = distPath;
|
|
2104
|
-
} else {
|
|
2105
|
-
DEMO_DATA_DIR = path2.join(process.cwd(), "resources", "employee-sqlite");
|
|
2106
|
-
if (!fs2.existsSync(DEMO_DATA_DIR)) {
|
|
2107
|
-
throw new Error(`Could not find employee-sqlite resources in any of the expected locations:
|
|
2108
|
-
- ${projectResourcesPath}
|
|
2109
|
-
- ${distPath}
|
|
2110
|
-
- ${DEMO_DATA_DIR}`);
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
function loadSqlFile(fileName) {
|
|
2114
|
-
const filePath = path2.join(DEMO_DATA_DIR, fileName);
|
|
2115
|
-
return fs2.readFileSync(filePath, "utf8");
|
|
2116
|
-
}
|
|
2117
|
-
function getSqliteInMemorySetupSql() {
|
|
2118
|
-
let sql2 = loadSqlFile("employee.sql");
|
|
2119
|
-
const readRegex = /\.read\s+([a-zA-Z0-9_]+\.sql)/g;
|
|
2120
|
-
let match;
|
|
2121
|
-
while ((match = readRegex.exec(sql2)) !== null) {
|
|
2122
|
-
const includePath = match[1];
|
|
2123
|
-
const includeContent = loadSqlFile(includePath);
|
|
2124
|
-
sql2 = sql2.replace(match[0], includeContent);
|
|
2125
|
-
}
|
|
2126
|
-
return sql2;
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
1836
|
// src/resources/index.ts
|
|
2130
1837
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2131
1838
|
|
|
@@ -2478,7 +2185,6 @@ var allowedKeywords = {
|
|
|
2478
2185
|
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
2479
2186
|
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2480
2187
|
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2481
|
-
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
2482
2188
|
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2483
2189
|
};
|
|
2484
2190
|
|
|
@@ -2546,9 +2252,6 @@ async function sqlGeneratorPromptHandler({
|
|
|
2546
2252
|
case "postgres":
|
|
2547
2253
|
sqlDialect = "postgres";
|
|
2548
2254
|
break;
|
|
2549
|
-
case "sqlite":
|
|
2550
|
-
sqlDialect = "sqlite";
|
|
2551
|
-
break;
|
|
2552
2255
|
case "mysql":
|
|
2553
2256
|
sqlDialect = "mysql";
|
|
2554
2257
|
break;
|
|
@@ -2609,11 +2312,6 @@ ${accessibleSchemas.map(
|
|
|
2609
2312
|
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
2610
2313
|
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
2611
2314
|
],
|
|
2612
|
-
sqlite: [
|
|
2613
|
-
"SELECT * FROM users WHERE created_at > datetime('now', '-1 day')",
|
|
2614
|
-
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
2615
|
-
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
2616
|
-
],
|
|
2617
2315
|
mysql: [
|
|
2618
2316
|
"SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY",
|
|
2619
2317
|
"SELECT u.name, COUNT(o.id) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name HAVING COUNT(o.id) > 5",
|
|
@@ -2945,9 +2643,9 @@ function registerPrompts(server) {
|
|
|
2945
2643
|
}
|
|
2946
2644
|
|
|
2947
2645
|
// src/server.ts
|
|
2948
|
-
var
|
|
2949
|
-
var
|
|
2950
|
-
var packageJsonPath =
|
|
2646
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2647
|
+
var __dirname2 = path2.dirname(__filename2);
|
|
2648
|
+
var packageJsonPath = path2.join(__dirname2, "..", "package.json");
|
|
2951
2649
|
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
2952
2650
|
var SERVER_NAME = "DBHub MCP Server";
|
|
2953
2651
|
var SERVER_VERSION = packageJson.version;
|
|
@@ -2974,8 +2672,7 @@ async function main() {
|
|
|
2974
2672
|
ERROR: Database connection string (DSN) is required.
|
|
2975
2673
|
Please provide the DSN in one of these ways (in order of priority):
|
|
2976
2674
|
|
|
2977
|
-
1.
|
|
2978
|
-
2. Command line argument: --dsn="your-connection-string"
|
|
2675
|
+
1. Command line argument: --dsn="your-connection-string"
|
|
2979
2676
|
3. Environment variable: export DSN="your-connection-string"
|
|
2980
2677
|
4. .env file: DSN=your-connection-string
|
|
2981
2678
|
|
|
@@ -2999,22 +2696,13 @@ See documentation for more details on configuring database connections.
|
|
|
2999
2696
|
const connectorManager = new ConnectorManager();
|
|
3000
2697
|
console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
|
|
3001
2698
|
console.error(`DSN source: ${dsnData.source}`);
|
|
3002
|
-
|
|
3003
|
-
const initScript = getSqliteInMemorySetupSql();
|
|
3004
|
-
await connectorManager.connectWithDSN(dsnData.dsn, initScript);
|
|
3005
|
-
} else {
|
|
3006
|
-
await connectorManager.connectWithDSN(dsnData.dsn);
|
|
3007
|
-
}
|
|
2699
|
+
await connectorManager.connectWithDSN(dsnData.dsn);
|
|
3008
2700
|
const transportData = resolveTransport();
|
|
3009
2701
|
console.error(`Using transport: ${transportData.type}`);
|
|
3010
2702
|
console.error(`Transport source: ${transportData.source}`);
|
|
3011
2703
|
const readonly = isReadOnlyMode();
|
|
3012
2704
|
const activeModes = [];
|
|
3013
2705
|
const modeDescriptions = [];
|
|
3014
|
-
if (dsnData.isDemo) {
|
|
3015
|
-
activeModes.push("DEMO");
|
|
3016
|
-
modeDescriptions.push("using sample employee database");
|
|
3017
|
-
}
|
|
3018
2706
|
if (readonly) {
|
|
3019
2707
|
activeModes.push("READ-ONLY");
|
|
3020
2708
|
modeDescriptions.push("only read only queries allowed");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Universal Database MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@azure/identity": "^4.8.0",
|
|
20
20
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
21
|
-
"better-sqlite3": "^11.9.0",
|
|
22
21
|
"dotenv": "^16.4.7",
|
|
23
22
|
"express": "^4.18.2",
|
|
24
23
|
"mariadb": "^3.4.0",
|
|
@@ -32,7 +31,6 @@
|
|
|
32
31
|
"@testcontainers/mssqlserver": "^11.0.3",
|
|
33
32
|
"@testcontainers/mysql": "^11.0.3",
|
|
34
33
|
"@testcontainers/postgresql": "^11.0.3",
|
|
35
|
-
"@types/better-sqlite3": "^7.6.12",
|
|
36
34
|
"@types/express": "^4.17.21",
|
|
37
35
|
"@types/mssql": "^9.1.7",
|
|
38
36
|
"@types/node": "^22.13.10",
|
|
@@ -61,7 +59,7 @@
|
|
|
61
59
|
"src/**/*"
|
|
62
60
|
],
|
|
63
61
|
"lint-staged": {
|
|
64
|
-
"*.{js,ts}": "
|
|
62
|
+
"*.{js,ts}": "pnpm run test:pre-commit"
|
|
65
63
|
},
|
|
66
64
|
"scripts": {
|
|
67
65
|
"build": "tsup",
|
|
@@ -71,6 +69,7 @@
|
|
|
71
69
|
"test": "vitest run",
|
|
72
70
|
"test:watch": "vitest",
|
|
73
71
|
"test:integration": "vitest run --testNamePattern='Integration Tests'",
|
|
72
|
+
"test:pre-commit": "vitest related --run --exclude '**/sqlserver.integration.test.ts'",
|
|
74
73
|
"pre-commit": "lint-staged"
|
|
75
74
|
}
|
|
76
75
|
}
|