@bytebase/dbhub 0.3.1 → 0.3.2
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 +22 -8
- package/dist/index.js +267 -203
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ https://demo.dbhub.ai/sse connects a [sample employee database](https://github.c
|
|
|
49
49
|
|
|
50
50
|
| Tool | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
|
|
51
51
|
| --------------- | ----------------- | :--------: | :---: | :-----: | :--------: | ------ |
|
|
52
|
-
| Execute
|
|
52
|
+
| Execute SQL | `execute_sql` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
53
53
|
| List Connectors | `list_connectors` | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
54
54
|
|
|
55
55
|
### Prompt Capabilities
|
|
@@ -151,6 +151,19 @@ npx @bytebase/dbhub --transport sse --port 8080 --demo
|
|
|
151
151
|
|
|
152
152
|
## Usage
|
|
153
153
|
|
|
154
|
+
### Read-only Mode
|
|
155
|
+
|
|
156
|
+
You can run DBHub in read-only mode, which restricts SQL query execution to read-only operations:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Enable read-only mode
|
|
160
|
+
npx @bytebase/dbhub --readonly --dsn "postgres://user:password@localhost:5432/dbname"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
In read-only mode, only [readonly SQL operations](https://github.com/bytebase/dbhub/blob/main/src/utils/allowed-keywords.ts) are allowed.
|
|
164
|
+
|
|
165
|
+
This provides an additional layer of security when connecting to production databases.
|
|
166
|
+
|
|
154
167
|
### Configure your database connection
|
|
155
168
|
|
|
156
169
|
You can use DBHub in demo mode with a sample employee database for testing:
|
|
@@ -186,13 +199,13 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
|
|
|
186
199
|
|
|
187
200
|
DBHub supports the following database connection string formats:
|
|
188
201
|
|
|
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`, `sqlite:C:/Users/YourName/data/database.db (windows)`
|
|
202
|
+
| Database | DSN Format | Example |
|
|
203
|
+
| ---------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
204
|
+
| MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname` |
|
|
205
|
+
| MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname` |
|
|
206
|
+
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
|
|
207
|
+
| SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname` |
|
|
208
|
+
| SQLite | `sqlite:///[path/to/file]` or `sqlite::memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite::memory:` |
|
|
196
209
|
|
|
197
210
|
#### SQL Server
|
|
198
211
|
|
|
@@ -223,6 +236,7 @@ Extra query parameters:
|
|
|
223
236
|
| dsn | Database connection string | Required if not in demo mode |
|
|
224
237
|
| transport | Transport mode: `stdio` or `sse` | `stdio` |
|
|
225
238
|
| port | HTTP server port (only applicable when using `--transport=sse`) | `8080` |
|
|
239
|
+
| readonly | Restrict SQL execution to read-only operations | `false` |
|
|
226
240
|
|
|
227
241
|
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.
|
|
228
242
|
|
package/dist/index.js
CHANGED
|
@@ -38,8 +38,8 @@ var _ConnectorRegistry = class _ConnectorRegistry {
|
|
|
38
38
|
/**
|
|
39
39
|
* Get sample DSN for a specific connector
|
|
40
40
|
*/
|
|
41
|
-
static getSampleDSN(
|
|
42
|
-
const connector = _ConnectorRegistry.getConnector(
|
|
41
|
+
static getSampleDSN(connectorType) {
|
|
42
|
+
const connector = _ConnectorRegistry.getConnector(connectorType);
|
|
43
43
|
if (!connector) return null;
|
|
44
44
|
return connector.dsnParser.getSampleDSN();
|
|
45
45
|
}
|
|
@@ -81,7 +81,9 @@ var PostgresDSNParser = class {
|
|
|
81
81
|
});
|
|
82
82
|
return config;
|
|
83
83
|
} catch (error) {
|
|
84
|
-
throw new Error(
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Failed to parse PostgreSQL DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
86
|
+
);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
getSampleDSN() {
|
|
@@ -145,12 +147,15 @@ var PostgresConnector = class {
|
|
|
145
147
|
const client = await this.pool.connect();
|
|
146
148
|
try {
|
|
147
149
|
const schemaToUse = schema || "public";
|
|
148
|
-
const result = await client.query(
|
|
150
|
+
const result = await client.query(
|
|
151
|
+
`
|
|
149
152
|
SELECT table_name
|
|
150
153
|
FROM information_schema.tables
|
|
151
154
|
WHERE table_schema = $1
|
|
152
155
|
ORDER BY table_name
|
|
153
|
-
`,
|
|
156
|
+
`,
|
|
157
|
+
[schemaToUse]
|
|
158
|
+
);
|
|
154
159
|
return result.rows.map((row) => row.table_name);
|
|
155
160
|
} finally {
|
|
156
161
|
client.release();
|
|
@@ -163,13 +168,16 @@ var PostgresConnector = class {
|
|
|
163
168
|
const client = await this.pool.connect();
|
|
164
169
|
try {
|
|
165
170
|
const schemaToUse = schema || "public";
|
|
166
|
-
const result = await client.query(
|
|
171
|
+
const result = await client.query(
|
|
172
|
+
`
|
|
167
173
|
SELECT EXISTS (
|
|
168
174
|
SELECT FROM information_schema.tables
|
|
169
175
|
WHERE table_schema = $1
|
|
170
176
|
AND table_name = $2
|
|
171
177
|
)
|
|
172
|
-
`,
|
|
178
|
+
`,
|
|
179
|
+
[schemaToUse, tableName]
|
|
180
|
+
);
|
|
173
181
|
return result.rows[0].exists;
|
|
174
182
|
} finally {
|
|
175
183
|
client.release();
|
|
@@ -182,7 +190,8 @@ var PostgresConnector = class {
|
|
|
182
190
|
const client = await this.pool.connect();
|
|
183
191
|
try {
|
|
184
192
|
const schemaToUse = schema || "public";
|
|
185
|
-
const result = await client.query(
|
|
193
|
+
const result = await client.query(
|
|
194
|
+
`
|
|
186
195
|
SELECT
|
|
187
196
|
i.relname as index_name,
|
|
188
197
|
array_agg(a.attname) as column_names,
|
|
@@ -209,7 +218,9 @@ var PostgresConnector = class {
|
|
|
209
218
|
ix.indisprimary
|
|
210
219
|
ORDER BY
|
|
211
220
|
i.relname
|
|
212
|
-
`,
|
|
221
|
+
`,
|
|
222
|
+
[tableName, schemaToUse]
|
|
223
|
+
);
|
|
213
224
|
return result.rows.map((row) => ({
|
|
214
225
|
index_name: row.index_name,
|
|
215
226
|
column_names: row.column_names,
|
|
@@ -227,7 +238,8 @@ var PostgresConnector = class {
|
|
|
227
238
|
const client = await this.pool.connect();
|
|
228
239
|
try {
|
|
229
240
|
const schemaToUse = schema || "public";
|
|
230
|
-
const result = await client.query(
|
|
241
|
+
const result = await client.query(
|
|
242
|
+
`
|
|
231
243
|
SELECT
|
|
232
244
|
column_name,
|
|
233
245
|
data_type,
|
|
@@ -237,7 +249,9 @@ var PostgresConnector = class {
|
|
|
237
249
|
WHERE table_schema = $1
|
|
238
250
|
AND table_name = $2
|
|
239
251
|
ORDER BY ordinal_position
|
|
240
|
-
`,
|
|
252
|
+
`,
|
|
253
|
+
[schemaToUse, tableName]
|
|
254
|
+
);
|
|
241
255
|
return result.rows;
|
|
242
256
|
} finally {
|
|
243
257
|
client.release();
|
|
@@ -250,13 +264,16 @@ var PostgresConnector = class {
|
|
|
250
264
|
const client = await this.pool.connect();
|
|
251
265
|
try {
|
|
252
266
|
const schemaToUse = schema || "public";
|
|
253
|
-
const result = await client.query(
|
|
267
|
+
const result = await client.query(
|
|
268
|
+
`
|
|
254
269
|
SELECT
|
|
255
270
|
routine_name
|
|
256
271
|
FROM information_schema.routines
|
|
257
272
|
WHERE routine_schema = $1
|
|
258
273
|
ORDER BY routine_name
|
|
259
|
-
`,
|
|
274
|
+
`,
|
|
275
|
+
[schemaToUse]
|
|
276
|
+
);
|
|
260
277
|
return result.rows.map((row) => row.routine_name);
|
|
261
278
|
} finally {
|
|
262
279
|
client.release();
|
|
@@ -269,7 +286,8 @@ var PostgresConnector = class {
|
|
|
269
286
|
const client = await this.pool.connect();
|
|
270
287
|
try {
|
|
271
288
|
const schemaToUse = schema || "public";
|
|
272
|
-
const result = await client.query(
|
|
289
|
+
const result = await client.query(
|
|
290
|
+
`
|
|
273
291
|
SELECT
|
|
274
292
|
routine_name as procedure_name,
|
|
275
293
|
routine_type,
|
|
@@ -292,20 +310,25 @@ var PostgresConnector = class {
|
|
|
292
310
|
FROM information_schema.routines
|
|
293
311
|
WHERE routine_schema = $1
|
|
294
312
|
AND routine_name = $2
|
|
295
|
-
`,
|
|
313
|
+
`,
|
|
314
|
+
[schemaToUse, procedureName]
|
|
315
|
+
);
|
|
296
316
|
if (result.rows.length === 0) {
|
|
297
317
|
throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`);
|
|
298
318
|
}
|
|
299
319
|
const procedure = result.rows[0];
|
|
300
320
|
let definition = procedure.definition;
|
|
301
321
|
try {
|
|
302
|
-
const oidResult = await client.query(
|
|
322
|
+
const oidResult = await client.query(
|
|
323
|
+
`
|
|
303
324
|
SELECT p.oid, p.prosrc
|
|
304
325
|
FROM pg_proc p
|
|
305
326
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
306
327
|
WHERE p.proname = $1
|
|
307
328
|
AND n.nspname = $2
|
|
308
|
-
`,
|
|
329
|
+
`,
|
|
330
|
+
[procedureName, schemaToUse]
|
|
331
|
+
);
|
|
309
332
|
if (oidResult.rows.length > 0) {
|
|
310
333
|
if (!definition) {
|
|
311
334
|
const oid = oidResult.rows[0].oid;
|
|
@@ -336,10 +359,6 @@ var PostgresConnector = class {
|
|
|
336
359
|
if (!this.pool) {
|
|
337
360
|
throw new Error("Not connected to database");
|
|
338
361
|
}
|
|
339
|
-
const safetyCheck = this.validateQuery(query);
|
|
340
|
-
if (!safetyCheck.isValid) {
|
|
341
|
-
throw new Error(safetyCheck.message || "Query validation failed");
|
|
342
|
-
}
|
|
343
362
|
const client = await this.pool.connect();
|
|
344
363
|
try {
|
|
345
364
|
return await client.query(query);
|
|
@@ -347,16 +366,6 @@ var PostgresConnector = class {
|
|
|
347
366
|
client.release();
|
|
348
367
|
}
|
|
349
368
|
}
|
|
350
|
-
validateQuery(query) {
|
|
351
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
352
|
-
if (!normalizedQuery.startsWith("select")) {
|
|
353
|
-
return {
|
|
354
|
-
isValid: false,
|
|
355
|
-
message: "Only SELECT queries are allowed for security reasons."
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
return { isValid: true };
|
|
359
|
-
}
|
|
360
369
|
};
|
|
361
370
|
var postgresConnector = new PostgresConnector();
|
|
362
371
|
ConnectorRegistry.register(postgresConnector);
|
|
@@ -367,7 +376,9 @@ import { DefaultAzureCredential } from "@azure/identity";
|
|
|
367
376
|
var SQLServerDSNParser = class {
|
|
368
377
|
async parse(dsn) {
|
|
369
378
|
if (!this.isValidDSN(dsn)) {
|
|
370
|
-
throw new Error(
|
|
379
|
+
throw new Error(
|
|
380
|
+
"Invalid SQL Server DSN format. Expected: sqlserver://username:password@host:port/database"
|
|
381
|
+
);
|
|
371
382
|
}
|
|
372
383
|
const url = new URL(dsn);
|
|
373
384
|
const host = url.hostname;
|
|
@@ -644,10 +655,12 @@ var SQLServerConnector = class {
|
|
|
644
655
|
const parameterResult = await request.query(parameterQuery);
|
|
645
656
|
let parameterList = "";
|
|
646
657
|
if (parameterResult.recordset.length > 0) {
|
|
647
|
-
parameterList = parameterResult.recordset.map(
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
658
|
+
parameterList = parameterResult.recordset.map(
|
|
659
|
+
(param) => {
|
|
660
|
+
const lengthStr = param.CHARACTER_MAXIMUM_LENGTH > 0 ? `(${param.CHARACTER_MAXIMUM_LENGTH})` : "";
|
|
661
|
+
return `${param.PARAMETER_NAME} ${param.PARAMETER_MODE} ${param.DATA_TYPE}${lengthStr}`;
|
|
662
|
+
}
|
|
663
|
+
).join(", ");
|
|
651
664
|
}
|
|
652
665
|
const definitionQuery = `
|
|
653
666
|
SELECT definition
|
|
@@ -679,10 +692,6 @@ var SQLServerConnector = class {
|
|
|
679
692
|
if (!this.connection) {
|
|
680
693
|
throw new Error("Not connected to SQL Server database");
|
|
681
694
|
}
|
|
682
|
-
const safetyCheck = this.validateQuery(query);
|
|
683
|
-
if (!safetyCheck.isValid) {
|
|
684
|
-
throw new Error(safetyCheck.message || "Query validation failed");
|
|
685
|
-
}
|
|
686
695
|
try {
|
|
687
696
|
const result = await this.connection.request().query(query);
|
|
688
697
|
return {
|
|
@@ -696,16 +705,6 @@ var SQLServerConnector = class {
|
|
|
696
705
|
throw new Error(`Failed to execute query: ${error.message}`);
|
|
697
706
|
}
|
|
698
707
|
}
|
|
699
|
-
validateQuery(query) {
|
|
700
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
701
|
-
if (!normalizedQuery.startsWith("select")) {
|
|
702
|
-
return {
|
|
703
|
-
isValid: false,
|
|
704
|
-
message: "Only SELECT queries are allowed for security reasons."
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
return { isValid: true };
|
|
708
|
-
}
|
|
709
708
|
};
|
|
710
709
|
var sqlServerConnector = new SQLServerConnector();
|
|
711
710
|
ConnectorRegistry.register(sqlServerConnector);
|
|
@@ -731,7 +730,9 @@ var SQLiteDSNParser = class {
|
|
|
731
730
|
}
|
|
732
731
|
return { dbPath };
|
|
733
732
|
} catch (error) {
|
|
734
|
-
throw new Error(
|
|
733
|
+
throw new Error(
|
|
734
|
+
`Failed to parse SQLite DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
735
|
+
);
|
|
735
736
|
}
|
|
736
737
|
}
|
|
737
738
|
getSampleDSN() {
|
|
@@ -792,11 +793,13 @@ var SQLiteConnector = class {
|
|
|
792
793
|
throw new Error("Not connected to SQLite database");
|
|
793
794
|
}
|
|
794
795
|
try {
|
|
795
|
-
const rows = this.db.prepare(
|
|
796
|
+
const rows = this.db.prepare(
|
|
797
|
+
`
|
|
796
798
|
SELECT name FROM sqlite_master
|
|
797
799
|
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
798
800
|
ORDER BY name
|
|
799
|
-
`
|
|
801
|
+
`
|
|
802
|
+
).all();
|
|
800
803
|
return rows.map((row) => row.name);
|
|
801
804
|
} catch (error) {
|
|
802
805
|
throw error;
|
|
@@ -807,10 +810,12 @@ var SQLiteConnector = class {
|
|
|
807
810
|
throw new Error("Not connected to SQLite database");
|
|
808
811
|
}
|
|
809
812
|
try {
|
|
810
|
-
const row = this.db.prepare(
|
|
813
|
+
const row = this.db.prepare(
|
|
814
|
+
`
|
|
811
815
|
SELECT name FROM sqlite_master
|
|
812
816
|
WHERE type='table' AND name = ?
|
|
813
|
-
`
|
|
817
|
+
`
|
|
818
|
+
).get(tableName);
|
|
814
819
|
return !!row;
|
|
815
820
|
} catch (error) {
|
|
816
821
|
throw error;
|
|
@@ -821,7 +826,8 @@ var SQLiteConnector = class {
|
|
|
821
826
|
throw new Error("Not connected to SQLite database");
|
|
822
827
|
}
|
|
823
828
|
try {
|
|
824
|
-
const indexInfoRows = this.db.prepare(
|
|
829
|
+
const indexInfoRows = this.db.prepare(
|
|
830
|
+
`
|
|
825
831
|
SELECT
|
|
826
832
|
name as index_name,
|
|
827
833
|
CASE
|
|
@@ -831,7 +837,8 @@ var SQLiteConnector = class {
|
|
|
831
837
|
FROM sqlite_master
|
|
832
838
|
WHERE type = 'index'
|
|
833
839
|
AND tbl_name = ?
|
|
834
|
-
`
|
|
840
|
+
`
|
|
841
|
+
).all(tableName);
|
|
835
842
|
const tableInfo = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
836
843
|
const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
|
|
837
844
|
const results = [];
|
|
@@ -886,16 +893,14 @@ var SQLiteConnector = class {
|
|
|
886
893
|
if (!this.db) {
|
|
887
894
|
throw new Error("Not connected to SQLite database");
|
|
888
895
|
}
|
|
889
|
-
throw new Error(
|
|
896
|
+
throw new Error(
|
|
897
|
+
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
898
|
+
);
|
|
890
899
|
}
|
|
891
900
|
async executeQuery(query) {
|
|
892
901
|
if (!this.db) {
|
|
893
902
|
throw new Error("Not connected to SQLite database");
|
|
894
903
|
}
|
|
895
|
-
const safetyCheck = this.validateQuery(query);
|
|
896
|
-
if (!safetyCheck.isValid) {
|
|
897
|
-
throw new Error(safetyCheck.message || "Query validation failed");
|
|
898
|
-
}
|
|
899
904
|
try {
|
|
900
905
|
const rows = this.db.prepare(query).all();
|
|
901
906
|
return { rows };
|
|
@@ -903,16 +908,6 @@ var SQLiteConnector = class {
|
|
|
903
908
|
throw error;
|
|
904
909
|
}
|
|
905
910
|
}
|
|
906
|
-
validateQuery(query) {
|
|
907
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
908
|
-
if (!normalizedQuery.startsWith("select")) {
|
|
909
|
-
return {
|
|
910
|
-
isValid: false,
|
|
911
|
-
message: "Only SELECT queries are allowed for security reasons."
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
return { isValid: true };
|
|
915
|
-
}
|
|
916
911
|
};
|
|
917
912
|
var sqliteConnector = new SQLiteConnector();
|
|
918
913
|
ConnectorRegistry.register(sqliteConnector);
|
|
@@ -941,7 +936,9 @@ var MySQLDSNParser = class {
|
|
|
941
936
|
});
|
|
942
937
|
return config;
|
|
943
938
|
} catch (error) {
|
|
944
|
-
throw new Error(
|
|
939
|
+
throw new Error(
|
|
940
|
+
`Failed to parse MySQL DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
941
|
+
);
|
|
945
942
|
}
|
|
946
943
|
}
|
|
947
944
|
getSampleDSN() {
|
|
@@ -1003,12 +1000,15 @@ var MySQLConnector = class {
|
|
|
1003
1000
|
try {
|
|
1004
1001
|
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1005
1002
|
const queryParams = schema ? [schema] : [];
|
|
1006
|
-
const [rows] = await this.pool.query(
|
|
1003
|
+
const [rows] = await this.pool.query(
|
|
1004
|
+
`
|
|
1007
1005
|
SELECT TABLE_NAME
|
|
1008
1006
|
FROM INFORMATION_SCHEMA.TABLES
|
|
1009
1007
|
${schemaClause}
|
|
1010
1008
|
ORDER BY TABLE_NAME
|
|
1011
|
-
`,
|
|
1009
|
+
`,
|
|
1010
|
+
queryParams
|
|
1011
|
+
);
|
|
1012
1012
|
return rows.map((row) => row.TABLE_NAME);
|
|
1013
1013
|
} catch (error) {
|
|
1014
1014
|
console.error("Error getting tables:", error);
|
|
@@ -1022,12 +1022,15 @@ var MySQLConnector = class {
|
|
|
1022
1022
|
try {
|
|
1023
1023
|
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1024
1024
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1025
|
-
const [rows] = await this.pool.query(
|
|
1025
|
+
const [rows] = await this.pool.query(
|
|
1026
|
+
`
|
|
1026
1027
|
SELECT COUNT(*) AS COUNT
|
|
1027
1028
|
FROM INFORMATION_SCHEMA.TABLES
|
|
1028
1029
|
${schemaClause}
|
|
1029
1030
|
AND TABLE_NAME = ?
|
|
1030
|
-
`,
|
|
1031
|
+
`,
|
|
1032
|
+
queryParams
|
|
1033
|
+
);
|
|
1031
1034
|
return rows[0].COUNT > 0;
|
|
1032
1035
|
} catch (error) {
|
|
1033
1036
|
console.error("Error checking if table exists:", error);
|
|
@@ -1041,7 +1044,8 @@ var MySQLConnector = class {
|
|
|
1041
1044
|
try {
|
|
1042
1045
|
const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
|
|
1043
1046
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1044
|
-
const [indexRows] = await this.pool.query(
|
|
1047
|
+
const [indexRows] = await this.pool.query(
|
|
1048
|
+
`
|
|
1045
1049
|
SELECT
|
|
1046
1050
|
INDEX_NAME,
|
|
1047
1051
|
COLUMN_NAME,
|
|
@@ -1055,7 +1059,9 @@ var MySQLConnector = class {
|
|
|
1055
1059
|
ORDER BY
|
|
1056
1060
|
INDEX_NAME,
|
|
1057
1061
|
SEQ_IN_INDEX
|
|
1058
|
-
`,
|
|
1062
|
+
`,
|
|
1063
|
+
queryParams
|
|
1064
|
+
);
|
|
1059
1065
|
const indexMap = /* @__PURE__ */ new Map();
|
|
1060
1066
|
for (const row of indexRows) {
|
|
1061
1067
|
const indexName = row.INDEX_NAME;
|
|
@@ -1094,7 +1100,8 @@ var MySQLConnector = class {
|
|
|
1094
1100
|
try {
|
|
1095
1101
|
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1096
1102
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1097
|
-
const [rows] = await this.pool.query(
|
|
1103
|
+
const [rows] = await this.pool.query(
|
|
1104
|
+
`
|
|
1098
1105
|
SELECT
|
|
1099
1106
|
COLUMN_NAME as column_name,
|
|
1100
1107
|
DATA_TYPE as data_type,
|
|
@@ -1104,7 +1111,9 @@ var MySQLConnector = class {
|
|
|
1104
1111
|
${schemaClause}
|
|
1105
1112
|
AND TABLE_NAME = ?
|
|
1106
1113
|
ORDER BY ORDINAL_POSITION
|
|
1107
|
-
`,
|
|
1114
|
+
`,
|
|
1115
|
+
queryParams
|
|
1116
|
+
);
|
|
1108
1117
|
return rows;
|
|
1109
1118
|
} catch (error) {
|
|
1110
1119
|
console.error("Error getting table schema:", error);
|
|
@@ -1118,12 +1127,15 @@ var MySQLConnector = class {
|
|
|
1118
1127
|
try {
|
|
1119
1128
|
const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
|
|
1120
1129
|
const queryParams = schema ? [schema] : [];
|
|
1121
|
-
const [rows] = await this.pool.query(
|
|
1130
|
+
const [rows] = await this.pool.query(
|
|
1131
|
+
`
|
|
1122
1132
|
SELECT ROUTINE_NAME
|
|
1123
1133
|
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1124
1134
|
${schemaClause}
|
|
1125
1135
|
ORDER BY ROUTINE_NAME
|
|
1126
|
-
`,
|
|
1136
|
+
`,
|
|
1137
|
+
queryParams
|
|
1138
|
+
);
|
|
1127
1139
|
return rows.map((row) => row.ROUTINE_NAME);
|
|
1128
1140
|
} catch (error) {
|
|
1129
1141
|
console.error("Error getting stored procedures:", error);
|
|
@@ -1137,7 +1149,8 @@ var MySQLConnector = class {
|
|
|
1137
1149
|
try {
|
|
1138
1150
|
const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
|
|
1139
1151
|
const queryParams = schema ? [schema, procedureName] : [procedureName];
|
|
1140
|
-
const [rows] = await this.pool.query(
|
|
1152
|
+
const [rows] = await this.pool.query(
|
|
1153
|
+
`
|
|
1141
1154
|
SELECT
|
|
1142
1155
|
r.ROUTINE_NAME AS procedure_name,
|
|
1143
1156
|
CASE
|
|
@@ -1161,7 +1174,9 @@ var MySQLConnector = class {
|
|
|
1161
1174
|
FROM INFORMATION_SCHEMA.ROUTINES r
|
|
1162
1175
|
${schemaClause}
|
|
1163
1176
|
AND r.ROUTINE_NAME = ?
|
|
1164
|
-
`,
|
|
1177
|
+
`,
|
|
1178
|
+
queryParams
|
|
1179
|
+
);
|
|
1165
1180
|
if (rows.length === 0) {
|
|
1166
1181
|
const schemaName = schema || "current schema";
|
|
1167
1182
|
throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
|
|
@@ -1194,11 +1209,14 @@ var MySQLConnector = class {
|
|
|
1194
1209
|
}
|
|
1195
1210
|
}
|
|
1196
1211
|
if (!definition) {
|
|
1197
|
-
const [bodyRows] = await this.pool.query(
|
|
1212
|
+
const [bodyRows] = await this.pool.query(
|
|
1213
|
+
`
|
|
1198
1214
|
SELECT ROUTINE_DEFINITION, ROUTINE_BODY
|
|
1199
1215
|
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1200
1216
|
WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
|
|
1201
|
-
`,
|
|
1217
|
+
`,
|
|
1218
|
+
[schemaValue, procedureName]
|
|
1219
|
+
);
|
|
1202
1220
|
if (bodyRows && bodyRows.length > 0) {
|
|
1203
1221
|
if (bodyRows[0].ROUTINE_DEFINITION) {
|
|
1204
1222
|
definition = bodyRows[0].ROUTINE_DEFINITION;
|
|
@@ -1233,10 +1251,6 @@ var MySQLConnector = class {
|
|
|
1233
1251
|
if (!this.pool) {
|
|
1234
1252
|
throw new Error("Not connected to database");
|
|
1235
1253
|
}
|
|
1236
|
-
const safetyCheck = this.validateQuery(query);
|
|
1237
|
-
if (!safetyCheck.isValid) {
|
|
1238
|
-
throw new Error(safetyCheck.message || "Query validation failed");
|
|
1239
|
-
}
|
|
1240
1254
|
try {
|
|
1241
1255
|
const [rows, fields] = await this.pool.query(query);
|
|
1242
1256
|
return { rows, fields };
|
|
@@ -1245,16 +1259,6 @@ var MySQLConnector = class {
|
|
|
1245
1259
|
throw error;
|
|
1246
1260
|
}
|
|
1247
1261
|
}
|
|
1248
|
-
validateQuery(query) {
|
|
1249
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
1250
|
-
if (!normalizedQuery.startsWith("select")) {
|
|
1251
|
-
return {
|
|
1252
|
-
isValid: false,
|
|
1253
|
-
message: "Only SELECT queries are allowed for security reasons."
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
return { isValid: true };
|
|
1257
|
-
}
|
|
1258
1262
|
};
|
|
1259
1263
|
var mysqlConnector = new MySQLConnector();
|
|
1260
1264
|
ConnectorRegistry.register(mysqlConnector);
|
|
@@ -1282,7 +1286,9 @@ var MariadbDSNParser = class {
|
|
|
1282
1286
|
});
|
|
1283
1287
|
return config;
|
|
1284
1288
|
} catch (error) {
|
|
1285
|
-
throw new Error(
|
|
1289
|
+
throw new Error(
|
|
1290
|
+
`Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
|
|
1291
|
+
);
|
|
1286
1292
|
}
|
|
1287
1293
|
}
|
|
1288
1294
|
getSampleDSN() {
|
|
@@ -1345,12 +1351,15 @@ var MariaDBConnector = class {
|
|
|
1345
1351
|
try {
|
|
1346
1352
|
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1347
1353
|
const queryParams = schema ? [schema] : [];
|
|
1348
|
-
const [rows] = await this.pool.query(
|
|
1354
|
+
const [rows] = await this.pool.query(
|
|
1355
|
+
`
|
|
1349
1356
|
SELECT TABLE_NAME
|
|
1350
1357
|
FROM INFORMATION_SCHEMA.TABLES
|
|
1351
1358
|
${schemaClause}
|
|
1352
1359
|
ORDER BY TABLE_NAME
|
|
1353
|
-
`,
|
|
1360
|
+
`,
|
|
1361
|
+
queryParams
|
|
1362
|
+
);
|
|
1354
1363
|
return rows.map((row) => row.TABLE_NAME);
|
|
1355
1364
|
} catch (error) {
|
|
1356
1365
|
console.error("Error getting tables:", error);
|
|
@@ -1364,12 +1373,15 @@ var MariaDBConnector = class {
|
|
|
1364
1373
|
try {
|
|
1365
1374
|
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1366
1375
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1367
|
-
const [rows] = await this.pool.query(
|
|
1376
|
+
const [rows] = await this.pool.query(
|
|
1377
|
+
`
|
|
1368
1378
|
SELECT COUNT(*) AS COUNT
|
|
1369
1379
|
FROM INFORMATION_SCHEMA.TABLES
|
|
1370
1380
|
${schemaClause}
|
|
1371
1381
|
AND TABLE_NAME = ?
|
|
1372
|
-
`,
|
|
1382
|
+
`,
|
|
1383
|
+
queryParams
|
|
1384
|
+
);
|
|
1373
1385
|
return rows[0].COUNT > 0;
|
|
1374
1386
|
} catch (error) {
|
|
1375
1387
|
console.error("Error checking if table exists:", error);
|
|
@@ -1383,7 +1395,8 @@ var MariaDBConnector = class {
|
|
|
1383
1395
|
try {
|
|
1384
1396
|
const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
|
|
1385
1397
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1386
|
-
const [indexRows] = await this.pool.query(
|
|
1398
|
+
const [indexRows] = await this.pool.query(
|
|
1399
|
+
`
|
|
1387
1400
|
SELECT
|
|
1388
1401
|
INDEX_NAME,
|
|
1389
1402
|
COLUMN_NAME,
|
|
@@ -1397,7 +1410,9 @@ var MariaDBConnector = class {
|
|
|
1397
1410
|
ORDER BY
|
|
1398
1411
|
INDEX_NAME,
|
|
1399
1412
|
SEQ_IN_INDEX
|
|
1400
|
-
`,
|
|
1413
|
+
`,
|
|
1414
|
+
queryParams
|
|
1415
|
+
);
|
|
1401
1416
|
const indexMap = /* @__PURE__ */ new Map();
|
|
1402
1417
|
for (const row of indexRows) {
|
|
1403
1418
|
const indexName = row.INDEX_NAME;
|
|
@@ -1436,7 +1451,8 @@ var MariaDBConnector = class {
|
|
|
1436
1451
|
try {
|
|
1437
1452
|
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
|
|
1438
1453
|
const queryParams = schema ? [schema, tableName] : [tableName];
|
|
1439
|
-
const [rows] = await this.pool.query(
|
|
1454
|
+
const [rows] = await this.pool.query(
|
|
1455
|
+
`
|
|
1440
1456
|
SELECT
|
|
1441
1457
|
COLUMN_NAME as column_name,
|
|
1442
1458
|
DATA_TYPE as data_type,
|
|
@@ -1446,7 +1462,9 @@ var MariaDBConnector = class {
|
|
|
1446
1462
|
${schemaClause}
|
|
1447
1463
|
AND TABLE_NAME = ?
|
|
1448
1464
|
ORDER BY ORDINAL_POSITION
|
|
1449
|
-
`,
|
|
1465
|
+
`,
|
|
1466
|
+
queryParams
|
|
1467
|
+
);
|
|
1450
1468
|
return rows;
|
|
1451
1469
|
} catch (error) {
|
|
1452
1470
|
console.error("Error getting table schema:", error);
|
|
@@ -1460,12 +1478,15 @@ var MariaDBConnector = class {
|
|
|
1460
1478
|
try {
|
|
1461
1479
|
const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
|
|
1462
1480
|
const queryParams = schema ? [schema] : [];
|
|
1463
|
-
const [rows] = await this.pool.query(
|
|
1481
|
+
const [rows] = await this.pool.query(
|
|
1482
|
+
`
|
|
1464
1483
|
SELECT ROUTINE_NAME
|
|
1465
1484
|
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1466
1485
|
${schemaClause}
|
|
1467
1486
|
ORDER BY ROUTINE_NAME
|
|
1468
|
-
`,
|
|
1487
|
+
`,
|
|
1488
|
+
queryParams
|
|
1489
|
+
);
|
|
1469
1490
|
return rows.map((row) => row.ROUTINE_NAME);
|
|
1470
1491
|
} catch (error) {
|
|
1471
1492
|
console.error("Error getting stored procedures:", error);
|
|
@@ -1479,7 +1500,8 @@ var MariaDBConnector = class {
|
|
|
1479
1500
|
try {
|
|
1480
1501
|
const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
|
|
1481
1502
|
const queryParams = schema ? [schema, procedureName] : [procedureName];
|
|
1482
|
-
const [rows] = await this.pool.query(
|
|
1503
|
+
const [rows] = await this.pool.query(
|
|
1504
|
+
`
|
|
1483
1505
|
SELECT
|
|
1484
1506
|
r.ROUTINE_NAME AS procedure_name,
|
|
1485
1507
|
CASE
|
|
@@ -1503,7 +1525,9 @@ var MariaDBConnector = class {
|
|
|
1503
1525
|
FROM INFORMATION_SCHEMA.ROUTINES r
|
|
1504
1526
|
${schemaClause}
|
|
1505
1527
|
AND r.ROUTINE_NAME = ?
|
|
1506
|
-
`,
|
|
1528
|
+
`,
|
|
1529
|
+
queryParams
|
|
1530
|
+
);
|
|
1507
1531
|
if (rows.length === 0) {
|
|
1508
1532
|
const schemaName = schema || "current schema";
|
|
1509
1533
|
throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
|
|
@@ -1536,11 +1560,14 @@ var MariaDBConnector = class {
|
|
|
1536
1560
|
}
|
|
1537
1561
|
}
|
|
1538
1562
|
if (!definition) {
|
|
1539
|
-
const [bodyRows] = await this.pool.query(
|
|
1563
|
+
const [bodyRows] = await this.pool.query(
|
|
1564
|
+
`
|
|
1540
1565
|
SELECT ROUTINE_DEFINITION, ROUTINE_BODY
|
|
1541
1566
|
FROM INFORMATION_SCHEMA.ROUTINES
|
|
1542
1567
|
WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
|
|
1543
|
-
`,
|
|
1568
|
+
`,
|
|
1569
|
+
[schemaValue, procedureName]
|
|
1570
|
+
);
|
|
1544
1571
|
if (bodyRows && bodyRows.length > 0) {
|
|
1545
1572
|
if (bodyRows[0].ROUTINE_DEFINITION) {
|
|
1546
1573
|
definition = bodyRows[0].ROUTINE_DEFINITION;
|
|
@@ -1575,10 +1602,6 @@ var MariaDBConnector = class {
|
|
|
1575
1602
|
if (!this.pool) {
|
|
1576
1603
|
throw new Error("Not connected to database");
|
|
1577
1604
|
}
|
|
1578
|
-
const safetyCheck = this.validateQuery(query);
|
|
1579
|
-
if (!safetyCheck.isValid) {
|
|
1580
|
-
throw new Error(safetyCheck.message || "Query validation failed");
|
|
1581
|
-
}
|
|
1582
1605
|
try {
|
|
1583
1606
|
const [rows, fields] = await this.pool.query(query);
|
|
1584
1607
|
return { rows, fields };
|
|
@@ -1587,16 +1610,6 @@ var MariaDBConnector = class {
|
|
|
1587
1610
|
throw error;
|
|
1588
1611
|
}
|
|
1589
1612
|
}
|
|
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
1613
|
};
|
|
1601
1614
|
var mariadbConnector = new MariaDBConnector();
|
|
1602
1615
|
ConnectorRegistry.register(mariadbConnector);
|
|
@@ -1746,6 +1759,16 @@ function isDemoMode() {
|
|
|
1746
1759
|
const args = parseCommandLineArgs();
|
|
1747
1760
|
return args.demo === "true";
|
|
1748
1761
|
}
|
|
1762
|
+
function isReadOnlyMode() {
|
|
1763
|
+
const args = parseCommandLineArgs();
|
|
1764
|
+
if (args.readonly !== void 0) {
|
|
1765
|
+
return args.readonly === "true";
|
|
1766
|
+
}
|
|
1767
|
+
if (process.env.READONLY !== void 0) {
|
|
1768
|
+
return process.env.READONLY === "true";
|
|
1769
|
+
}
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1749
1772
|
function resolveDSN() {
|
|
1750
1773
|
const args = parseCommandLineArgs();
|
|
1751
1774
|
if (isDemoMode()) {
|
|
@@ -1863,39 +1886,47 @@ function formatErrorResponse(error, code = "ERROR", details) {
|
|
|
1863
1886
|
}
|
|
1864
1887
|
function createToolErrorResponse(error, code = "ERROR", details) {
|
|
1865
1888
|
return {
|
|
1866
|
-
content: [
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1889
|
+
content: [
|
|
1890
|
+
{
|
|
1891
|
+
type: "text",
|
|
1892
|
+
text: JSON.stringify(formatErrorResponse(error, code, details), null, 2),
|
|
1893
|
+
mimeType: "application/json"
|
|
1894
|
+
}
|
|
1895
|
+
],
|
|
1871
1896
|
isError: true
|
|
1872
1897
|
};
|
|
1873
1898
|
}
|
|
1874
1899
|
function createToolSuccessResponse(data, meta = {}) {
|
|
1875
1900
|
return {
|
|
1876
|
-
content: [
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1901
|
+
content: [
|
|
1902
|
+
{
|
|
1903
|
+
type: "text",
|
|
1904
|
+
text: JSON.stringify(formatSuccessResponse(data, meta), null, 2),
|
|
1905
|
+
mimeType: "application/json"
|
|
1906
|
+
}
|
|
1907
|
+
]
|
|
1881
1908
|
};
|
|
1882
1909
|
}
|
|
1883
1910
|
function createResourceErrorResponse(uri, error, code = "ERROR", details) {
|
|
1884
1911
|
return {
|
|
1885
|
-
contents: [
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1912
|
+
contents: [
|
|
1913
|
+
{
|
|
1914
|
+
uri,
|
|
1915
|
+
text: JSON.stringify(formatErrorResponse(error, code, details), null, 2),
|
|
1916
|
+
mimeType: "application/json"
|
|
1917
|
+
}
|
|
1918
|
+
]
|
|
1890
1919
|
};
|
|
1891
1920
|
}
|
|
1892
1921
|
function createResourceSuccessResponse(uri, data, meta = {}) {
|
|
1893
1922
|
return {
|
|
1894
|
-
contents: [
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1923
|
+
contents: [
|
|
1924
|
+
{
|
|
1925
|
+
uri,
|
|
1926
|
+
text: JSON.stringify(formatSuccessResponse(data, meta), null, 2),
|
|
1927
|
+
mimeType: "application/json"
|
|
1928
|
+
}
|
|
1929
|
+
]
|
|
1899
1930
|
};
|
|
1900
1931
|
}
|
|
1901
1932
|
function formatPromptSuccessResponse(text, references = []) {
|
|
@@ -2032,11 +2063,7 @@ async function indexesResourceHandler(uri, variables, _extra) {
|
|
|
2032
2063
|
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
2033
2064
|
const tableName = variables && variables.tableName ? Array.isArray(variables.tableName) ? variables.tableName[0] : variables.tableName : void 0;
|
|
2034
2065
|
if (!tableName) {
|
|
2035
|
-
return createResourceErrorResponse(
|
|
2036
|
-
uri.href,
|
|
2037
|
-
"Table name is required",
|
|
2038
|
-
"MISSING_TABLE_NAME"
|
|
2039
|
-
);
|
|
2066
|
+
return createResourceErrorResponse(uri.href, "Table name is required", "MISSING_TABLE_NAME");
|
|
2040
2067
|
}
|
|
2041
2068
|
try {
|
|
2042
2069
|
if (schemaName) {
|
|
@@ -2109,11 +2136,7 @@ async function procedureDetailResourceHandler(uri, variables, _extra) {
|
|
|
2109
2136
|
const schemaName = variables && variables.schemaName ? Array.isArray(variables.schemaName) ? variables.schemaName[0] : variables.schemaName : void 0;
|
|
2110
2137
|
const procedureName = variables && variables.procedureName ? Array.isArray(variables.procedureName) ? variables.procedureName[0] : variables.procedureName : void 0;
|
|
2111
2138
|
if (!procedureName) {
|
|
2112
|
-
return createResourceErrorResponse(
|
|
2113
|
-
uri.href,
|
|
2114
|
-
"Procedure name is required",
|
|
2115
|
-
"MISSING_PARAMETER"
|
|
2116
|
-
);
|
|
2139
|
+
return createResourceErrorResponse(uri.href, "Procedure name is required", "MISSING_PARAMETER");
|
|
2117
2140
|
}
|
|
2118
2141
|
try {
|
|
2119
2142
|
if (schemaName) {
|
|
@@ -2148,11 +2171,7 @@ async function procedureDetailResourceHandler(uri, variables, _extra) {
|
|
|
2148
2171
|
|
|
2149
2172
|
// src/resources/index.ts
|
|
2150
2173
|
function registerResources(server) {
|
|
2151
|
-
server.resource(
|
|
2152
|
-
"schemas",
|
|
2153
|
-
"db://schemas",
|
|
2154
|
-
schemasResourceHandler
|
|
2155
|
-
);
|
|
2174
|
+
server.resource("schemas", "db://schemas", schemasResourceHandler);
|
|
2156
2175
|
server.resource(
|
|
2157
2176
|
"tables_in_schema",
|
|
2158
2177
|
new ResourceTemplate("db://schemas/{schemaName}/tables", { list: void 0 }),
|
|
@@ -2165,7 +2184,9 @@ function registerResources(server) {
|
|
|
2165
2184
|
);
|
|
2166
2185
|
server.resource(
|
|
2167
2186
|
"indexes_in_table",
|
|
2168
|
-
new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}/indexes", {
|
|
2187
|
+
new ResourceTemplate("db://schemas/{schemaName}/tables/{tableName}/indexes", {
|
|
2188
|
+
list: void 0
|
|
2189
|
+
}),
|
|
2169
2190
|
indexesResourceHandler
|
|
2170
2191
|
);
|
|
2171
2192
|
server.resource(
|
|
@@ -2175,24 +2196,42 @@ function registerResources(server) {
|
|
|
2175
2196
|
);
|
|
2176
2197
|
server.resource(
|
|
2177
2198
|
"procedure_detail_in_schema",
|
|
2178
|
-
new ResourceTemplate("db://schemas/{schemaName}/procedures/{procedureName}", {
|
|
2199
|
+
new ResourceTemplate("db://schemas/{schemaName}/procedures/{procedureName}", {
|
|
2200
|
+
list: void 0
|
|
2201
|
+
}),
|
|
2179
2202
|
procedureDetailResourceHandler
|
|
2180
2203
|
);
|
|
2181
2204
|
}
|
|
2182
2205
|
|
|
2183
|
-
// src/tools/
|
|
2206
|
+
// src/tools/execute-sql.ts
|
|
2184
2207
|
import { z } from "zod";
|
|
2185
|
-
|
|
2208
|
+
|
|
2209
|
+
// src/utils/allowed-keywords.ts
|
|
2210
|
+
var allowedKeywords = {
|
|
2211
|
+
postgres: ["select", "with", "explain", "analyze", "show"],
|
|
2212
|
+
mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2213
|
+
mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
|
|
2214
|
+
sqlite: ["select", "with", "explain", "analyze", "pragma"],
|
|
2215
|
+
sqlserver: ["select", "with", "explain", "showplan"]
|
|
2216
|
+
};
|
|
2217
|
+
|
|
2218
|
+
// src/tools/execute-sql.ts
|
|
2219
|
+
var executeSqlSchema = {
|
|
2186
2220
|
query: z.string().describe("SQL query to execute (SELECT only)")
|
|
2187
2221
|
};
|
|
2188
|
-
|
|
2222
|
+
function isReadOnlyQuery(query, connectorType) {
|
|
2223
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
2224
|
+
const firstWord = normalizedQuery.split(/\s+/)[0];
|
|
2225
|
+
const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
|
|
2226
|
+
return keywordList.includes(firstWord);
|
|
2227
|
+
}
|
|
2228
|
+
async function executeSqlToolHandler({ query }, _extra) {
|
|
2189
2229
|
const connector = ConnectorManager.getCurrentConnector();
|
|
2190
2230
|
try {
|
|
2191
|
-
|
|
2192
|
-
if (!validationResult.isValid) {
|
|
2231
|
+
if (isReadOnlyMode() && !isReadOnlyQuery(query, connector.id)) {
|
|
2193
2232
|
return createToolErrorResponse(
|
|
2194
|
-
|
|
2195
|
-
"
|
|
2233
|
+
`Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`,
|
|
2234
|
+
"READONLY_VIOLATION"
|
|
2196
2235
|
);
|
|
2197
2236
|
}
|
|
2198
2237
|
const result = await connector.executeQuery(query);
|
|
@@ -2202,36 +2241,34 @@ async function runQueryToolHandler({ query }, _extra) {
|
|
|
2202
2241
|
};
|
|
2203
2242
|
return createToolSuccessResponse(responseData);
|
|
2204
2243
|
} catch (error) {
|
|
2205
|
-
return createToolErrorResponse(
|
|
2206
|
-
error.message,
|
|
2207
|
-
"EXECUTION_ERROR"
|
|
2208
|
-
);
|
|
2244
|
+
return createToolErrorResponse(error.message, "EXECUTION_ERROR");
|
|
2209
2245
|
}
|
|
2210
2246
|
}
|
|
2211
2247
|
|
|
2212
2248
|
// src/tools/list-connectors.ts
|
|
2213
2249
|
async function listConnectorsToolHandler(_args, _extra) {
|
|
2214
2250
|
const samples = ConnectorRegistry.getAllSampleDSNs();
|
|
2215
|
-
let
|
|
2251
|
+
let activeConnectorType = null;
|
|
2216
2252
|
try {
|
|
2217
2253
|
const activeConnector = ConnectorManager.getCurrentConnector();
|
|
2218
|
-
|
|
2254
|
+
activeConnectorType = activeConnector.id;
|
|
2219
2255
|
} catch (error) {
|
|
2220
2256
|
}
|
|
2221
2257
|
const isDemo = isDemoMode();
|
|
2222
|
-
if (isDemo && !
|
|
2223
|
-
|
|
2258
|
+
if (isDemo && !activeConnectorType) {
|
|
2259
|
+
activeConnectorType = "sqlite";
|
|
2224
2260
|
}
|
|
2225
2261
|
const sampleObjects = Object.entries(samples).map(([id, dsn]) => ({
|
|
2226
2262
|
id,
|
|
2227
2263
|
dsn,
|
|
2228
|
-
active: id ===
|
|
2264
|
+
active: id === activeConnectorType
|
|
2229
2265
|
}));
|
|
2230
2266
|
const responseData = {
|
|
2231
2267
|
connectors: sampleObjects,
|
|
2232
2268
|
count: sampleObjects.length,
|
|
2233
|
-
activeConnector:
|
|
2234
|
-
demoMode: isDemo
|
|
2269
|
+
activeConnector: activeConnectorType,
|
|
2270
|
+
demoMode: isDemo,
|
|
2271
|
+
readonlyMode: isReadOnlyMode()
|
|
2235
2272
|
};
|
|
2236
2273
|
return createToolSuccessResponse(responseData);
|
|
2237
2274
|
}
|
|
@@ -2239,10 +2276,10 @@ async function listConnectorsToolHandler(_args, _extra) {
|
|
|
2239
2276
|
// src/tools/index.ts
|
|
2240
2277
|
function registerTools(server) {
|
|
2241
2278
|
server.tool(
|
|
2242
|
-
"
|
|
2243
|
-
"
|
|
2244
|
-
|
|
2245
|
-
|
|
2279
|
+
"execute_sql",
|
|
2280
|
+
"Execute a SQL query on the current database",
|
|
2281
|
+
executeSqlSchema,
|
|
2282
|
+
executeSqlToolHandler
|
|
2246
2283
|
);
|
|
2247
2284
|
server.tool(
|
|
2248
2285
|
"list_connectors",
|
|
@@ -2258,7 +2295,10 @@ var sqlGeneratorSchema = {
|
|
|
2258
2295
|
description: z2.string().describe("Natural language description of the SQL query to generate"),
|
|
2259
2296
|
schema: z2.string().optional().describe("Optional database schema to use")
|
|
2260
2297
|
};
|
|
2261
|
-
async function sqlGeneratorPromptHandler({
|
|
2298
|
+
async function sqlGeneratorPromptHandler({
|
|
2299
|
+
description,
|
|
2300
|
+
schema
|
|
2301
|
+
}, _extra) {
|
|
2262
2302
|
try {
|
|
2263
2303
|
const connector = ConnectorManager.getCurrentConnector();
|
|
2264
2304
|
let sqlDialect;
|
|
@@ -2321,9 +2361,7 @@ async function sqlGeneratorPromptHandler({ description, schema }, _extra) {
|
|
|
2321
2361
|
}
|
|
2322
2362
|
const schemaContext = accessibleSchemas.length > 0 ? `Available tables and their columns:
|
|
2323
2363
|
${accessibleSchemas.map(
|
|
2324
|
-
(schema2) => `- ${schema2.table}: ${schema2.columns.map(
|
|
2325
|
-
(col) => `${col.name} (${col.type})`
|
|
2326
|
-
).join(", ")}`
|
|
2364
|
+
(schema2) => `- ${schema2.table}: ${schema2.columns.map((col) => `${col.name} (${col.type})`).join(", ")}`
|
|
2327
2365
|
).join("\n")}` : "No schema information available.";
|
|
2328
2366
|
const dialectExamples = {
|
|
2329
2367
|
postgres: [
|
|
@@ -2375,7 +2413,11 @@ SELECT COUNT(*) AS count
|
|
|
2375
2413
|
FROM ${accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name"};`;
|
|
2376
2414
|
} else if (description.toLowerCase().includes("average") || description.toLowerCase().includes("avg")) {
|
|
2377
2415
|
const table = accessibleSchemas.length > 0 ? accessibleSchemas[0].table : "table_name";
|
|
2378
|
-
const numericColumn = accessibleSchemas.length > 0 ? accessibleSchemas[0].columns.find(
|
|
2416
|
+
const numericColumn = accessibleSchemas.length > 0 ? accessibleSchemas[0].columns.find(
|
|
2417
|
+
(col) => ["int", "numeric", "decimal", "float", "real", "double"].some(
|
|
2418
|
+
(t) => col.type.includes(t)
|
|
2419
|
+
)
|
|
2420
|
+
)?.name || "numeric_column" : "numeric_column";
|
|
2379
2421
|
const schemaPrefix = schema ? `-- Schema: ${schema}
|
|
2380
2422
|
` : "";
|
|
2381
2423
|
generatedSQL = `${schemaPrefix}-- Average query generated from: "${description}"
|
|
@@ -2423,7 +2465,10 @@ var dbExplainerSchema = {
|
|
|
2423
2465
|
schema: z3.string().optional().describe("Optional database schema to use"),
|
|
2424
2466
|
table: z3.string().optional().describe("Optional specific table to explain")
|
|
2425
2467
|
};
|
|
2426
|
-
async function dbExplainerPromptHandler({
|
|
2468
|
+
async function dbExplainerPromptHandler({
|
|
2469
|
+
schema,
|
|
2470
|
+
table
|
|
2471
|
+
}, _extra) {
|
|
2427
2472
|
try {
|
|
2428
2473
|
const connector = ConnectorManager.getCurrentConnector();
|
|
2429
2474
|
if (schema) {
|
|
@@ -2477,7 +2522,9 @@ ${determineRelationships(matchingTable, columns)}`;
|
|
|
2477
2522
|
}
|
|
2478
2523
|
try {
|
|
2479
2524
|
const columns = await connector.getTableSchema(tableName, schema);
|
|
2480
|
-
const column = columns.find(
|
|
2525
|
+
const column = columns.find(
|
|
2526
|
+
(c) => c.column_name.toLowerCase() === columnName.toLowerCase()
|
|
2527
|
+
);
|
|
2481
2528
|
if (column) {
|
|
2482
2529
|
const columnDescription = `Column: ${tableName}.${column.column_name}
|
|
2483
2530
|
|
|
@@ -2568,11 +2615,15 @@ function determineRelationships(tableName, columns) {
|
|
|
2568
2615
|
if (idColumns.length > 0) {
|
|
2569
2616
|
idColumns.forEach((col) => {
|
|
2570
2617
|
const referencedTable = col.column_name.toLowerCase().replace("_id", "");
|
|
2571
|
-
potentialRelationships.push(
|
|
2618
|
+
potentialRelationships.push(
|
|
2619
|
+
`May have a relationship with the "${referencedTable}" table (via ${col.column_name})`
|
|
2620
|
+
);
|
|
2572
2621
|
});
|
|
2573
2622
|
}
|
|
2574
2623
|
if (columns.some((c) => c.column_name.toLowerCase() === "id")) {
|
|
2575
|
-
potentialRelationships.push(
|
|
2624
|
+
potentialRelationships.push(
|
|
2625
|
+
`May be referenced by other tables as "${tableName.toLowerCase()}_id"`
|
|
2626
|
+
);
|
|
2576
2627
|
}
|
|
2577
2628
|
return potentialRelationships.length > 0 ? potentialRelationships.join("\n") : "No obvious relationships identified based on column names";
|
|
2578
2629
|
}
|
|
@@ -2660,8 +2711,8 @@ var packageJsonPath = path3.join(__dirname3, "..", "package.json");
|
|
|
2660
2711
|
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
2661
2712
|
var SERVER_NAME = "DBHub MCP Server";
|
|
2662
2713
|
var SERVER_VERSION = packageJson.version;
|
|
2663
|
-
function generateBanner(version,
|
|
2664
|
-
const
|
|
2714
|
+
function generateBanner(version, modes = []) {
|
|
2715
|
+
const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
|
|
2665
2716
|
return `
|
|
2666
2717
|
_____ ____ _ _ _
|
|
2667
2718
|
| __ \\| _ \\| | | | | |
|
|
@@ -2670,7 +2721,7 @@ function generateBanner(version, isDemo = false) {
|
|
|
2670
2721
|
| |__| | |_) | | | | |_| | |_) |
|
|
2671
2722
|
|_____/|____/|_| |_|\\__,_|_.__/
|
|
2672
2723
|
|
|
2673
|
-
v${version}${
|
|
2724
|
+
v${version}${modeText} - Universal Database MCP Server
|
|
2674
2725
|
`;
|
|
2675
2726
|
}
|
|
2676
2727
|
async function main() {
|
|
@@ -2706,7 +2757,6 @@ See documentation for more details on configuring database connections.
|
|
|
2706
2757
|
console.error(`Connecting with DSN: ${redactDSN(dsnData.dsn)}`);
|
|
2707
2758
|
console.error(`DSN source: ${dsnData.source}`);
|
|
2708
2759
|
if (dsnData.isDemo) {
|
|
2709
|
-
console.error("Running in demo mode with sample employee database");
|
|
2710
2760
|
const initScript = getSqliteInMemorySetupSql();
|
|
2711
2761
|
await connectorManager.connectWithDSN(dsnData.dsn, initScript);
|
|
2712
2762
|
} else {
|
|
@@ -2715,7 +2765,21 @@ See documentation for more details on configuring database connections.
|
|
|
2715
2765
|
const transportData = resolveTransport();
|
|
2716
2766
|
console.error(`Using transport: ${transportData.type}`);
|
|
2717
2767
|
console.error(`Transport source: ${transportData.source}`);
|
|
2718
|
-
|
|
2768
|
+
const readonly = isReadOnlyMode();
|
|
2769
|
+
const activeModes = [];
|
|
2770
|
+
const modeDescriptions = [];
|
|
2771
|
+
if (dsnData.isDemo) {
|
|
2772
|
+
activeModes.push("DEMO");
|
|
2773
|
+
modeDescriptions.push("using sample employee database");
|
|
2774
|
+
}
|
|
2775
|
+
if (readonly) {
|
|
2776
|
+
activeModes.push("READ-ONLY");
|
|
2777
|
+
modeDescriptions.push("only read only queries allowed");
|
|
2778
|
+
}
|
|
2779
|
+
if (activeModes.length > 0) {
|
|
2780
|
+
console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
|
|
2781
|
+
}
|
|
2782
|
+
console.error(generateBanner(SERVER_VERSION, activeModes));
|
|
2719
2783
|
if (transportData.type === "sse") {
|
|
2720
2784
|
const app = express();
|
|
2721
2785
|
let transport;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Universal Database MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"better-sqlite3": "^11.9.0",
|
|
22
22
|
"dotenv": "^16.4.7",
|
|
23
23
|
"express": "^4.18.2",
|
|
24
|
-
"mssql": "^11.0.1",
|
|
25
24
|
"mariadb": "^3.4.0",
|
|
25
|
+
"mssql": "^11.0.1",
|
|
26
26
|
"mysql2": "^3.13.0",
|
|
27
27
|
"pg": "^8.13.3",
|
|
28
28
|
"zod": "^3.24.2"
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@types/node": "^22.13.10",
|
|
35
35
|
"@types/pg": "^8.11.11",
|
|
36
36
|
"cross-env": "^7.0.3",
|
|
37
|
+
"prettier": "^3.5.3",
|
|
37
38
|
"ts-node": "^10.9.2",
|
|
38
39
|
"tsup": "^8.4.0",
|
|
39
40
|
"tsx": "^4.19.3",
|