@bytebase/dbhub 0.9.0 → 0.10.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 +55 -21
- package/dist/index.js +319 -7
- package/dist/resources/employee-sqlite/employee.sql +117 -0
- package/dist/resources/employee-sqlite/load_department.sql +10 -0
- package/dist/resources/employee-sqlite/load_dept_emp.sql +1103 -0
- package/dist/resources/employee-sqlite/load_dept_manager.sql +17 -0
- package/dist/resources/employee-sqlite/load_employee.sql +1000 -0
- package/dist/resources/employee-sqlite/load_salary1.sql +9488 -0
- package/dist/resources/employee-sqlite/load_title.sql +1470 -0
- package/dist/resources/employee-sqlite/object.sql +74 -0
- package/dist/resources/employee-sqlite/show_elapsed.sql +4 -0
- package/dist/resources/employee-sqlite/test_employee_md5.sql +119 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@ 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
|
+
| | | | | |
|
|
27
29
|
| | | +--->+ MySQL |
|
|
28
30
|
| | | | | |
|
|
29
31
|
| | | +--->+ MariaDB |
|
|
@@ -42,27 +44,27 @@ https://demo.dbhub.ai/message connects a [sample employee database](https://gith
|
|
|
42
44
|
|
|
43
45
|
### Database Resources
|
|
44
46
|
|
|
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}` | ✅ | ✅ | ✅ | ✅ |
|
|
47
|
+
| Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
|
|
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}` | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
53
55
|
|
|
54
56
|
### Database Tools
|
|
55
57
|
|
|
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) | ✅ | ✅ | ✅ | ✅ |
|
|
58
|
+
| Tool | Command Name | Description | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
|
|
59
|
+
| ----------- | ------------- | ------------------------------------------------------------------- | :--------: | :---: | :-----: | :--------: | ------ |
|
|
60
|
+
| Execute SQL | `execute_sql` | Execute single or multiple SQL statements (separated by semicolons) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
59
61
|
|
|
60
62
|
### Prompt Capabilities
|
|
61
63
|
|
|
62
|
-
| Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server |
|
|
63
|
-
| ------------------- | -------------- | :--------: | :---: | :-----: | :--------: |
|
|
64
|
-
| Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ |
|
|
65
|
-
| Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ |
|
|
64
|
+
| Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
|
|
65
|
+
| ------------------- | -------------- | :--------: | :---: | :-----: | :--------: | ------ |
|
|
66
|
+
| Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
67
|
+
| Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
66
68
|
|
|
67
69
|
## Installation
|
|
68
70
|
|
|
@@ -79,6 +81,16 @@ docker run --rm --init \
|
|
|
79
81
|
--dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
80
82
|
```
|
|
81
83
|
|
|
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
|
+
```
|
|
82
94
|
|
|
83
95
|
|
|
84
96
|
### NPM
|
|
@@ -88,6 +100,12 @@ docker run --rm --init \
|
|
|
88
100
|
npx @bytebase/dbhub --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
89
101
|
```
|
|
90
102
|
|
|
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.
|
|
91
109
|
|
|
92
110
|
### Claude Desktop
|
|
93
111
|
|
|
@@ -124,6 +142,10 @@ npx @bytebase/dbhub --transport http --port 8080 --dsn "postgres://user:password
|
|
|
124
142
|
"postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
125
143
|
]
|
|
126
144
|
},
|
|
145
|
+
"dbhub-demo": {
|
|
146
|
+
"command": "npx",
|
|
147
|
+
"args": ["-y", "@bytebase/dbhub", "--transport", "stdio", "--demo"]
|
|
148
|
+
}
|
|
127
149
|
}
|
|
128
150
|
}
|
|
129
151
|
```
|
|
@@ -149,6 +171,7 @@ You can specify the SSL mode using the `sslmode` parameter in your DSN string:
|
|
|
149
171
|
| MySQL | ✅ | ✅ | Certificate verification |
|
|
150
172
|
| MariaDB | ✅ | ✅ | Certificate verification |
|
|
151
173
|
| SQL Server | ✅ | ✅ | Certificate verification |
|
|
174
|
+
| SQLite | ❌ | ❌ | N/A (file-based) |
|
|
152
175
|
|
|
153
176
|
**SSL Mode Options:**
|
|
154
177
|
|
|
@@ -185,6 +208,12 @@ This provides an additional layer of security when connecting to production data
|
|
|
185
208
|
|
|
186
209
|
### Configure your database connection
|
|
187
210
|
|
|
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
|
+
|
|
188
217
|
> [!WARNING]
|
|
189
218
|
> If your user/password contains special characters, you need to escape them first. (e.g. `pass#word` should be escaped as `pass%23word`)
|
|
190
219
|
|
|
@@ -221,6 +250,7 @@ DBHub supports the following database connection string formats:
|
|
|
221
250
|
| MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname?sslmode=disable` |
|
|
222
251
|
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
|
|
223
252
|
| 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:` |
|
|
224
254
|
|
|
225
255
|
|
|
226
256
|
#### SQL Server
|
|
@@ -246,13 +276,15 @@ Extra query parameters:
|
|
|
246
276
|
|
|
247
277
|
### Command line options
|
|
248
278
|
|
|
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`
|
|
279
|
+
| Option | Environment Variable | Description | Default |
|
|
280
|
+
| --------- | -------------------- | ---------------------------------------------------------------- | ---------------------------- |
|
|
281
|
+
| dsn | `DSN` | Database connection string | Required if not in demo mode |
|
|
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` |
|
|
255
286
|
|
|
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.
|
|
256
288
|
|
|
257
289
|
## Development
|
|
258
290
|
|
|
@@ -310,6 +342,8 @@ pnpm test src/connectors/__tests__/mysql.integration.test.ts
|
|
|
310
342
|
pnpm test src/connectors/__tests__/mariadb.integration.test.ts
|
|
311
343
|
# Run only SQL Server integration tests
|
|
312
344
|
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
|
|
313
347
|
# Run JSON RPC integration tests
|
|
314
348
|
pnpm test src/__tests__/json-rpc-integration.test.ts
|
|
315
349
|
```
|
package/dist/index.js
CHANGED
|
@@ -159,6 +159,9 @@ function obfuscateDSNPassword(dsn) {
|
|
|
159
159
|
return dsn;
|
|
160
160
|
}
|
|
161
161
|
const protocol = protocolMatch[1];
|
|
162
|
+
if (protocol === "sqlite") {
|
|
163
|
+
return dsn;
|
|
164
|
+
}
|
|
162
165
|
const protocolPart = dsn.split("://")[1];
|
|
163
166
|
if (!protocolPart) {
|
|
164
167
|
return dsn;
|
|
@@ -876,6 +879,246 @@ var SQLServerConnector = class {
|
|
|
876
879
|
var sqlServerConnector = new SQLServerConnector();
|
|
877
880
|
ConnectorRegistry.register(sqlServerConnector);
|
|
878
881
|
|
|
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
|
+
|
|
879
1122
|
// src/connectors/mysql/index.ts
|
|
880
1123
|
import mysql from "mysql2/promise";
|
|
881
1124
|
var MySQLDSNParser = class {
|
|
@@ -1637,9 +1880,9 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
|
|
|
1637
1880
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1638
1881
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1639
1882
|
import express from "express";
|
|
1640
|
-
import
|
|
1883
|
+
import path3 from "path";
|
|
1641
1884
|
import { readFileSync } from "fs";
|
|
1642
|
-
import { fileURLToPath as
|
|
1885
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1643
1886
|
|
|
1644
1887
|
// src/connectors/manager.ts
|
|
1645
1888
|
var managerInstance = null;
|
|
@@ -1773,6 +2016,10 @@ function loadEnvFiles() {
|
|
|
1773
2016
|
}
|
|
1774
2017
|
return null;
|
|
1775
2018
|
}
|
|
2019
|
+
function isDemoMode() {
|
|
2020
|
+
const args = parseCommandLineArgs();
|
|
2021
|
+
return args.demo === "true";
|
|
2022
|
+
}
|
|
1776
2023
|
function isReadOnlyMode() {
|
|
1777
2024
|
const args = parseCommandLineArgs();
|
|
1778
2025
|
if (args.readonly !== void 0) {
|
|
@@ -1785,6 +2032,13 @@ function isReadOnlyMode() {
|
|
|
1785
2032
|
}
|
|
1786
2033
|
function resolveDSN() {
|
|
1787
2034
|
const args = parseCommandLineArgs();
|
|
2035
|
+
if (isDemoMode()) {
|
|
2036
|
+
return {
|
|
2037
|
+
dsn: "sqlite:///:memory:",
|
|
2038
|
+
source: "demo mode",
|
|
2039
|
+
isDemo: true
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
1788
2042
|
if (args.dsn) {
|
|
1789
2043
|
return { dsn: args.dsn, source: "command line argument" };
|
|
1790
2044
|
}
|
|
@@ -1833,6 +2087,45 @@ function redactDSN(dsn) {
|
|
|
1833
2087
|
}
|
|
1834
2088
|
}
|
|
1835
2089
|
|
|
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
|
+
|
|
1836
2129
|
// src/resources/index.ts
|
|
1837
2130
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1838
2131
|
|
|
@@ -2185,6 +2478,7 @@ var allowedKeywords = {
|
|
|
2185
2478
|
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
2186
2479
|
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2187
2480
|
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2481
|
+
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
2188
2482
|
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2189
2483
|
};
|
|
2190
2484
|
|
|
@@ -2252,6 +2546,9 @@ async function sqlGeneratorPromptHandler({
|
|
|
2252
2546
|
case "postgres":
|
|
2253
2547
|
sqlDialect = "postgres";
|
|
2254
2548
|
break;
|
|
2549
|
+
case "sqlite":
|
|
2550
|
+
sqlDialect = "sqlite";
|
|
2551
|
+
break;
|
|
2255
2552
|
case "mysql":
|
|
2256
2553
|
sqlDialect = "mysql";
|
|
2257
2554
|
break;
|
|
@@ -2312,6 +2609,11 @@ ${accessibleSchemas.map(
|
|
|
2312
2609
|
"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",
|
|
2313
2610
|
"SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products)"
|
|
2314
2611
|
],
|
|
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
|
+
],
|
|
2315
2617
|
mysql: [
|
|
2316
2618
|
"SELECT * FROM users WHERE created_at > NOW() - INTERVAL 1 DAY",
|
|
2317
2619
|
"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",
|
|
@@ -2643,9 +2945,9 @@ function registerPrompts(server) {
|
|
|
2643
2945
|
}
|
|
2644
2946
|
|
|
2645
2947
|
// src/server.ts
|
|
2646
|
-
var
|
|
2647
|
-
var
|
|
2648
|
-
var packageJsonPath =
|
|
2948
|
+
var __filename3 = fileURLToPath3(import.meta.url);
|
|
2949
|
+
var __dirname3 = path3.dirname(__filename3);
|
|
2950
|
+
var packageJsonPath = path3.join(__dirname3, "..", "package.json");
|
|
2649
2951
|
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
2650
2952
|
var SERVER_NAME = "DBHub MCP Server";
|
|
2651
2953
|
var SERVER_VERSION = packageJson.version;
|
|
@@ -2672,7 +2974,8 @@ async function main() {
|
|
|
2672
2974
|
ERROR: Database connection string (DSN) is required.
|
|
2673
2975
|
Please provide the DSN in one of these ways (in order of priority):
|
|
2674
2976
|
|
|
2675
|
-
1.
|
|
2977
|
+
1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
|
|
2978
|
+
2. Command line argument: --dsn="your-connection-string"
|
|
2676
2979
|
3. Environment variable: export DSN="your-connection-string"
|
|
2677
2980
|
4. .env file: DSN=your-connection-string
|
|
2678
2981
|
|
|
@@ -2696,13 +2999,22 @@ See documentation for more details on configuring database connections.
|
|
|
2696
2999
|
const connectorManager = new ConnectorManager();
|
|
2697
3000
|
console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
|
|
2698
3001
|
console.error(`DSN source: ${dsnData.source}`);
|
|
2699
|
-
|
|
3002
|
+
if (dsnData.isDemo) {
|
|
3003
|
+
const initScript = getSqliteInMemorySetupSql();
|
|
3004
|
+
await connectorManager.connectWithDSN(dsnData.dsn, initScript);
|
|
3005
|
+
} else {
|
|
3006
|
+
await connectorManager.connectWithDSN(dsnData.dsn);
|
|
3007
|
+
}
|
|
2700
3008
|
const transportData = resolveTransport();
|
|
2701
3009
|
console.error(`Using transport: ${transportData.type}`);
|
|
2702
3010
|
console.error(`Transport source: ${transportData.source}`);
|
|
2703
3011
|
const readonly = isReadOnlyMode();
|
|
2704
3012
|
const activeModes = [];
|
|
2705
3013
|
const modeDescriptions = [];
|
|
3014
|
+
if (dsnData.isDemo) {
|
|
3015
|
+
activeModes.push("DEMO");
|
|
3016
|
+
modeDescriptions.push("using sample employee database");
|
|
3017
|
+
}
|
|
2706
3018
|
if (readonly) {
|
|
2707
3019
|
activeModes.push("READ-ONLY");
|
|
2708
3020
|
modeDescriptions.push("only read only queries allowed");
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
-- Sample employee database
|
|
2
|
+
-- See changelog table for details
|
|
3
|
+
-- Copyright (C) 2007,2008, MySQL AB
|
|
4
|
+
--
|
|
5
|
+
-- Original data created by Fusheng Wang and Carlo Zaniolo
|
|
6
|
+
-- http://www.cs.aau.dk/TimeCenter/software.htm
|
|
7
|
+
-- http://www.cs.aau.dk/TimeCenter/Data/employeeTemporalDataSet.zip
|
|
8
|
+
--
|
|
9
|
+
-- Current schema by Giuseppe Maxia
|
|
10
|
+
-- Data conversion from XML to relational by Patrick Crews
|
|
11
|
+
-- SQLite adaptation by Claude Code
|
|
12
|
+
--
|
|
13
|
+
-- This work is licensed under the
|
|
14
|
+
-- Creative Commons Attribution-Share Alike 3.0 Unported License.
|
|
15
|
+
-- To view a copy of this license, visit
|
|
16
|
+
-- http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
|
|
17
|
+
-- Creative Commons, 171 Second Street, Suite 300, San Francisco,
|
|
18
|
+
-- California, 94105, USA.
|
|
19
|
+
--
|
|
20
|
+
-- DISCLAIMER
|
|
21
|
+
-- To the best of our knowledge, this data is fabricated, and
|
|
22
|
+
-- it does not correspond to real people.
|
|
23
|
+
-- Any similarity to existing people is purely coincidental.
|
|
24
|
+
--
|
|
25
|
+
|
|
26
|
+
PRAGMA foreign_keys = ON;
|
|
27
|
+
|
|
28
|
+
SELECT 'CREATING DATABASE STRUCTURE' as 'INFO';
|
|
29
|
+
|
|
30
|
+
DROP TABLE IF EXISTS dept_emp;
|
|
31
|
+
DROP TABLE IF EXISTS dept_manager;
|
|
32
|
+
DROP TABLE IF EXISTS title;
|
|
33
|
+
DROP TABLE IF EXISTS salary;
|
|
34
|
+
DROP TABLE IF EXISTS employee;
|
|
35
|
+
DROP TABLE IF EXISTS department;
|
|
36
|
+
DROP VIEW IF EXISTS dept_emp_latest_date;
|
|
37
|
+
DROP VIEW IF EXISTS current_dept_emp;
|
|
38
|
+
|
|
39
|
+
CREATE TABLE employee (
|
|
40
|
+
emp_no INTEGER NOT NULL,
|
|
41
|
+
birth_date DATE NOT NULL,
|
|
42
|
+
first_name TEXT NOT NULL,
|
|
43
|
+
last_name TEXT NOT NULL,
|
|
44
|
+
gender TEXT NOT NULL CHECK (gender IN ('M','F')),
|
|
45
|
+
hire_date DATE NOT NULL,
|
|
46
|
+
PRIMARY KEY (emp_no)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE department (
|
|
50
|
+
dept_no TEXT NOT NULL,
|
|
51
|
+
dept_name TEXT NOT NULL,
|
|
52
|
+
PRIMARY KEY (dept_no),
|
|
53
|
+
UNIQUE (dept_name)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE dept_manager (
|
|
57
|
+
emp_no INTEGER NOT NULL,
|
|
58
|
+
dept_no TEXT NOT NULL,
|
|
59
|
+
from_date DATE NOT NULL,
|
|
60
|
+
to_date DATE NOT NULL,
|
|
61
|
+
FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE,
|
|
62
|
+
FOREIGN KEY (dept_no) REFERENCES department (dept_no) ON DELETE CASCADE,
|
|
63
|
+
PRIMARY KEY (emp_no,dept_no)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE dept_emp (
|
|
67
|
+
emp_no INTEGER NOT NULL,
|
|
68
|
+
dept_no TEXT NOT NULL,
|
|
69
|
+
from_date DATE NOT NULL,
|
|
70
|
+
to_date DATE NOT NULL,
|
|
71
|
+
FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE,
|
|
72
|
+
FOREIGN KEY (dept_no) REFERENCES department (dept_no) ON DELETE CASCADE,
|
|
73
|
+
PRIMARY KEY (emp_no,dept_no)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE title (
|
|
77
|
+
emp_no INTEGER NOT NULL,
|
|
78
|
+
title TEXT NOT NULL,
|
|
79
|
+
from_date DATE NOT NULL,
|
|
80
|
+
to_date DATE,
|
|
81
|
+
FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE,
|
|
82
|
+
PRIMARY KEY (emp_no,title,from_date)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE salary (
|
|
86
|
+
emp_no INTEGER NOT NULL,
|
|
87
|
+
amount INTEGER NOT NULL,
|
|
88
|
+
from_date DATE NOT NULL,
|
|
89
|
+
to_date DATE NOT NULL,
|
|
90
|
+
FOREIGN KEY (emp_no) REFERENCES employee (emp_no) ON DELETE CASCADE,
|
|
91
|
+
PRIMARY KEY (emp_no,from_date)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE VIEW dept_emp_latest_date AS
|
|
95
|
+
SELECT emp_no, MAX(from_date) AS from_date, MAX(to_date) AS to_date
|
|
96
|
+
FROM dept_emp
|
|
97
|
+
GROUP BY emp_no;
|
|
98
|
+
|
|
99
|
+
-- shows only the current department for each employee
|
|
100
|
+
CREATE VIEW current_dept_emp AS
|
|
101
|
+
SELECT l.emp_no, dept_no, l.from_date, l.to_date
|
|
102
|
+
FROM dept_emp d
|
|
103
|
+
INNER JOIN dept_emp_latest_date l
|
|
104
|
+
ON d.emp_no=l.emp_no AND d.from_date=l.from_date AND l.to_date = d.to_date;
|
|
105
|
+
|
|
106
|
+
SELECT 'LOADING department' as 'INFO';
|
|
107
|
+
.read load_department.sql
|
|
108
|
+
SELECT 'LOADING employee' as 'INFO';
|
|
109
|
+
.read load_employee.sql
|
|
110
|
+
SELECT 'LOADING dept_emp' as 'INFO';
|
|
111
|
+
.read load_dept_emp.sql
|
|
112
|
+
SELECT 'LOADING dept_manager' as 'INFO';
|
|
113
|
+
.read load_dept_manager.sql
|
|
114
|
+
SELECT 'LOADING title' as 'INFO';
|
|
115
|
+
.read load_title.sql
|
|
116
|
+
SELECT 'LOADING salary' as 'INFO';
|
|
117
|
+
.read load_salary1.sql
|