@bytebase/dbhub 0.11.1 → 0.11.3
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 +30 -2
- package/dist/index.js +199 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,10 +10,18 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<a href="https://cursor.com/install-mcp?name=dbhub&config=eyJjb21tYW5kIjoibnB4IEBieXRlYmFzZS9kYmh1YiIsImVudiI6eyJUUkFOU1BPUlQiOiJzdGRpbyIsIkRTTiI6InBvc3RncmVzOi8vdXNlcjpwYXNzd29yZEBsb2NhbGhvc3Q6NTQzMi9kYm5hbWU%2Fc3NsbW9kZT1kaXNhYmxlIiwiUkVBRE9OTFkiOiJ0cnVlIn19"><img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Add dbhub MCP server to Cursor" height="32" /></a>
|
|
14
13
|
<a href="https://discord.gg/BjEkZpsJzn"><img src="https://img.shields.io/badge/%20-Hang%20out%20on%20Discord-5865F2?style=for-the-badge&logo=discord&labelColor=EEEEEE" alt="Join our Discord" height="32" /></a>
|
|
15
14
|
</p>
|
|
16
15
|
|
|
16
|
+
<p>
|
|
17
|
+
Add to Cursor by copying the below link to browser
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
cursor://anysphere.cursor-deeplink/mcp/install?name=dbhub&config=eyJjb21tYW5kIjoibnB4IEBieXRlYmFzZS9kYmh1YiIsImVudiI6eyJUUkFOU1BPUlQiOiJzdGRpbyIsIkRTTiI6InBvc3RncmVzOi8vdXNlcjpwYXNzd29yZEBsb2NhbGhvc3Q6NTQzMi9kYm5hbWU%2Fc3NsbW9kZT1kaXNhYmxlIiwiUkVBRE9OTFkiOiJ0cnVlIn19
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
</p>
|
|
24
|
+
|
|
17
25
|
DBHub is a universal database gateway implementing the Model Context Protocol (MCP) server interface. This gateway allows MCP-compatible clients to connect to and explore different databases.
|
|
18
26
|
|
|
19
27
|
```bash
|
|
@@ -159,7 +167,14 @@ Check https://docs.anthropic.com/en/docs/claude-code/mcp
|
|
|
159
167
|
|
|
160
168
|
### Cursor
|
|
161
169
|
|
|
162
|
-
|
|
170
|
+
<p>
|
|
171
|
+
Add to Cursor by copying the below link to browser
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
cursor://anysphere.cursor-deeplink/mcp/install?name=dbhub&config=eyJjb21tYW5kIjoibnB4IEBieXRlYmFzZS9kYmh1YiIsImVudiI6eyJUUkFOU1BPUlQiOiJzdGRpbyIsIkRTTiI6InBvc3RncmVzOi8vdXNlcjpwYXNzd29yZEBsb2NhbGhvc3Q6NTQzMi9kYm5hbWU%2Fc3NsbW9kZT1kaXNhYmxlIiwiUkVBRE9OTFkiOiJ0cnVlIn19
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
</p>
|
|
163
178
|
|
|
164
179
|

|
|
165
180
|
|
|
@@ -181,6 +196,18 @@ In read-only mode, only [readonly SQL operations](https://github.com/bytebase/db
|
|
|
181
196
|
|
|
182
197
|
This provides an additional layer of security when connecting to production databases.
|
|
183
198
|
|
|
199
|
+
### Row Limiting
|
|
200
|
+
|
|
201
|
+
You can limit the number of rows returned from SELECT queries using the `--max-rows` parameter. This helps prevent accidentally retrieving too much data from large tables:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Limit SELECT queries to return at most 1000 rows
|
|
205
|
+
npx @bytebase/dbhub --dsn "postgres://user:password@localhost:5432/dbname" --max-rows 1000
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- Row limiting is only applied to SELECT statements, not INSERT/UPDATE/DELETE
|
|
209
|
+
- If your query already has a `LIMIT` or `TOP` clause, DBHub uses the smaller value
|
|
210
|
+
|
|
184
211
|
### SSL Connections
|
|
185
212
|
|
|
186
213
|
You can specify the SSL mode using the `sslmode` parameter in your DSN string:
|
|
@@ -353,6 +380,7 @@ Extra query parameters:
|
|
|
353
380
|
| transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
|
|
354
381
|
| port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
|
|
355
382
|
| readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
|
|
383
|
+
| max-rows | N/A | Limit the number of rows returned from SELECT queries | No limit |
|
|
356
384
|
| demo | N/A | Run in demo mode with sample employee database | `false` |
|
|
357
385
|
| ssh-host | `SSH_HOST` | SSH server hostname for tunnel connection | N/A |
|
|
358
386
|
| ssh-port | `SSH_PORT` | SSH server port | `22` |
|
package/dist/index.js
CHANGED
|
@@ -185,6 +185,97 @@ function obfuscateDSNPassword(dsn) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// src/utils/sql-row-limiter.ts
|
|
189
|
+
var SQLRowLimiter = class {
|
|
190
|
+
/**
|
|
191
|
+
* Check if a SQL statement is a SELECT query that can benefit from row limiting
|
|
192
|
+
* Only handles SELECT queries
|
|
193
|
+
*/
|
|
194
|
+
static isSelectQuery(sql2) {
|
|
195
|
+
const trimmed = sql2.trim().toLowerCase();
|
|
196
|
+
return trimmed.startsWith("select");
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if a SQL statement already has a LIMIT clause
|
|
200
|
+
*/
|
|
201
|
+
static hasLimitClause(sql2) {
|
|
202
|
+
const limitRegex = /\blimit\s+\d+/i;
|
|
203
|
+
return limitRegex.test(sql2);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if a SQL statement already has a TOP clause (SQL Server)
|
|
207
|
+
*/
|
|
208
|
+
static hasTopClause(sql2) {
|
|
209
|
+
const topRegex = /\bselect\s+top\s+\d+/i;
|
|
210
|
+
return topRegex.test(sql2);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Extract existing LIMIT value from SQL if present
|
|
214
|
+
*/
|
|
215
|
+
static extractLimitValue(sql2) {
|
|
216
|
+
const limitMatch = sql2.match(/\blimit\s+(\d+)/i);
|
|
217
|
+
if (limitMatch) {
|
|
218
|
+
return parseInt(limitMatch[1], 10);
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Extract existing TOP value from SQL if present (SQL Server)
|
|
224
|
+
*/
|
|
225
|
+
static extractTopValue(sql2) {
|
|
226
|
+
const topMatch = sql2.match(/\bselect\s+top\s+(\d+)/i);
|
|
227
|
+
if (topMatch) {
|
|
228
|
+
return parseInt(topMatch[1], 10);
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Add or modify LIMIT clause in a SQL statement
|
|
234
|
+
*/
|
|
235
|
+
static applyLimitToQuery(sql2, maxRows) {
|
|
236
|
+
const existingLimit = this.extractLimitValue(sql2);
|
|
237
|
+
if (existingLimit !== null) {
|
|
238
|
+
const effectiveLimit = Math.min(existingLimit, maxRows);
|
|
239
|
+
return sql2.replace(/\blimit\s+\d+/i, `LIMIT ${effectiveLimit}`);
|
|
240
|
+
} else {
|
|
241
|
+
const trimmed = sql2.trim();
|
|
242
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
243
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
244
|
+
return `${sqlWithoutSemicolon} LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Add or modify TOP clause in a SQL statement (SQL Server)
|
|
249
|
+
*/
|
|
250
|
+
static applyTopToQuery(sql2, maxRows) {
|
|
251
|
+
const existingTop = this.extractTopValue(sql2);
|
|
252
|
+
if (existingTop !== null) {
|
|
253
|
+
const effectiveTop = Math.min(existingTop, maxRows);
|
|
254
|
+
return sql2.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
|
|
255
|
+
} else {
|
|
256
|
+
return sql2.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Apply maxRows limit to a SELECT query only
|
|
261
|
+
*/
|
|
262
|
+
static applyMaxRows(sql2, maxRows) {
|
|
263
|
+
if (!maxRows || !this.isSelectQuery(sql2)) {
|
|
264
|
+
return sql2;
|
|
265
|
+
}
|
|
266
|
+
return this.applyLimitToQuery(sql2, maxRows);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Apply maxRows limit to a SELECT query using SQL Server TOP syntax
|
|
270
|
+
*/
|
|
271
|
+
static applyMaxRowsForSQLServer(sql2, maxRows) {
|
|
272
|
+
if (!maxRows || !this.isSelectQuery(sql2)) {
|
|
273
|
+
return sql2;
|
|
274
|
+
}
|
|
275
|
+
return this.applyTopToQuery(sql2, maxRows);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
188
279
|
// src/connectors/postgres/index.ts
|
|
189
280
|
var { Pool } = pg;
|
|
190
281
|
var PostgresDSNParser = class {
|
|
@@ -494,7 +585,7 @@ var PostgresConnector = class {
|
|
|
494
585
|
client.release();
|
|
495
586
|
}
|
|
496
587
|
}
|
|
497
|
-
async executeSQL(sql2) {
|
|
588
|
+
async executeSQL(sql2, options) {
|
|
498
589
|
if (!this.pool) {
|
|
499
590
|
throw new Error("Not connected to database");
|
|
500
591
|
}
|
|
@@ -502,13 +593,15 @@ var PostgresConnector = class {
|
|
|
502
593
|
try {
|
|
503
594
|
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
504
595
|
if (statements.length === 1) {
|
|
505
|
-
|
|
596
|
+
const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
|
|
597
|
+
return await client.query(processedStatement);
|
|
506
598
|
} else {
|
|
507
599
|
let allRows = [];
|
|
508
600
|
await client.query("BEGIN");
|
|
509
601
|
try {
|
|
510
|
-
for (
|
|
511
|
-
const
|
|
602
|
+
for (let statement of statements) {
|
|
603
|
+
const processedStatement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
|
|
604
|
+
const result = await client.query(processedStatement);
|
|
512
605
|
if (result.rows && result.rows.length > 0) {
|
|
513
606
|
allRows.push(...result.rows);
|
|
514
607
|
}
|
|
@@ -858,12 +951,16 @@ var SQLServerConnector = class {
|
|
|
858
951
|
throw new Error(`Failed to get stored procedure details: ${error.message}`);
|
|
859
952
|
}
|
|
860
953
|
}
|
|
861
|
-
async executeSQL(sql2) {
|
|
954
|
+
async executeSQL(sql2, options) {
|
|
862
955
|
if (!this.connection) {
|
|
863
956
|
throw new Error("Not connected to SQL Server database");
|
|
864
957
|
}
|
|
865
958
|
try {
|
|
866
|
-
|
|
959
|
+
let processedSQL = sql2;
|
|
960
|
+
if (options.maxRows) {
|
|
961
|
+
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sql2, options.maxRows);
|
|
962
|
+
}
|
|
963
|
+
const result = await this.connection.request().query(processedSQL);
|
|
867
964
|
return {
|
|
868
965
|
rows: result.recordset || [],
|
|
869
966
|
fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
|
|
@@ -1074,20 +1171,24 @@ var SQLiteConnector = class {
|
|
|
1074
1171
|
"SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
|
|
1075
1172
|
);
|
|
1076
1173
|
}
|
|
1077
|
-
async executeSQL(sql2) {
|
|
1174
|
+
async executeSQL(sql2, options) {
|
|
1078
1175
|
if (!this.db) {
|
|
1079
1176
|
throw new Error("Not connected to SQLite database");
|
|
1080
1177
|
}
|
|
1081
1178
|
try {
|
|
1082
1179
|
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1083
1180
|
if (statements.length === 1) {
|
|
1181
|
+
let processedStatement = statements[0];
|
|
1084
1182
|
const trimmedStatement = statements[0].toLowerCase().trim();
|
|
1085
1183
|
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"));
|
|
1184
|
+
if (options.maxRows) {
|
|
1185
|
+
processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows);
|
|
1186
|
+
}
|
|
1086
1187
|
if (isReadStatement) {
|
|
1087
|
-
const rows = this.db.prepare(
|
|
1188
|
+
const rows = this.db.prepare(processedStatement).all();
|
|
1088
1189
|
return { rows };
|
|
1089
1190
|
} else {
|
|
1090
|
-
this.db.prepare(
|
|
1191
|
+
this.db.prepare(processedStatement).run();
|
|
1091
1192
|
return { rows: [] };
|
|
1092
1193
|
}
|
|
1093
1194
|
} else {
|
|
@@ -1105,7 +1206,8 @@ var SQLiteConnector = class {
|
|
|
1105
1206
|
this.db.exec(writeStatements.join("; "));
|
|
1106
1207
|
}
|
|
1107
1208
|
let allRows = [];
|
|
1108
|
-
for (
|
|
1209
|
+
for (let statement of readStatements) {
|
|
1210
|
+
statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
|
|
1109
1211
|
const result = this.db.prepare(statement).all();
|
|
1110
1212
|
allRows.push(...result);
|
|
1111
1213
|
}
|
|
@@ -1467,18 +1569,29 @@ var MySQLConnector = class {
|
|
|
1467
1569
|
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
1468
1570
|
return rows[0].DB;
|
|
1469
1571
|
}
|
|
1470
|
-
async executeSQL(sql2) {
|
|
1572
|
+
async executeSQL(sql2, options) {
|
|
1471
1573
|
if (!this.pool) {
|
|
1472
1574
|
throw new Error("Not connected to database");
|
|
1473
1575
|
}
|
|
1474
1576
|
try {
|
|
1475
|
-
|
|
1577
|
+
let processedSQL = sql2;
|
|
1578
|
+
if (options.maxRows) {
|
|
1579
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1580
|
+
const processedStatements = statements.map(
|
|
1581
|
+
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
1582
|
+
);
|
|
1583
|
+
processedSQL = processedStatements.join("; ");
|
|
1584
|
+
if (sql2.trim().endsWith(";")) {
|
|
1585
|
+
processedSQL += ";";
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
const results = await this.pool.query(processedSQL);
|
|
1476
1589
|
const [firstResult] = results;
|
|
1477
|
-
if (Array.isArray(firstResult) && firstResult.length > 0 && Array.isArray(firstResult[0])
|
|
1590
|
+
if (Array.isArray(firstResult) && firstResult.length > 0 && Array.isArray(firstResult[0])) {
|
|
1478
1591
|
let allRows = [];
|
|
1479
|
-
for (const
|
|
1480
|
-
if (Array.isArray(
|
|
1481
|
-
allRows.push(...
|
|
1592
|
+
for (const result of firstResult) {
|
|
1593
|
+
if (Array.isArray(result)) {
|
|
1594
|
+
allRows.push(...result);
|
|
1482
1595
|
}
|
|
1483
1596
|
}
|
|
1484
1597
|
return { rows: allRows };
|
|
@@ -1845,12 +1958,23 @@ var MariaDBConnector = class {
|
|
|
1845
1958
|
const rows = await this.pool.query("SELECT DATABASE() AS DB");
|
|
1846
1959
|
return rows[0].DB;
|
|
1847
1960
|
}
|
|
1848
|
-
async executeSQL(sql2) {
|
|
1961
|
+
async executeSQL(sql2, options) {
|
|
1849
1962
|
if (!this.pool) {
|
|
1850
1963
|
throw new Error("Not connected to database");
|
|
1851
1964
|
}
|
|
1852
1965
|
try {
|
|
1853
|
-
|
|
1966
|
+
let processedSQL = sql2;
|
|
1967
|
+
if (options.maxRows) {
|
|
1968
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1969
|
+
const processedStatements = statements.map(
|
|
1970
|
+
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
1971
|
+
);
|
|
1972
|
+
processedSQL = processedStatements.join("; ");
|
|
1973
|
+
if (sql2.trim().endsWith(";")) {
|
|
1974
|
+
processedSQL += ";";
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
const results = await this.pool.query(processedSQL);
|
|
1854
1978
|
if (Array.isArray(results)) {
|
|
1855
1979
|
if (results.length > 0 && Array.isArray(results[0]) && results[0].length > 0) {
|
|
1856
1980
|
let allRows = [];
|
|
@@ -2031,6 +2155,7 @@ import dotenv from "dotenv";
|
|
|
2031
2155
|
import path from "path";
|
|
2032
2156
|
import fs from "fs";
|
|
2033
2157
|
import { fileURLToPath } from "url";
|
|
2158
|
+
import { homedir as homedir2 } from "os";
|
|
2034
2159
|
|
|
2035
2160
|
// src/utils/ssh-config-parser.ts
|
|
2036
2161
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
@@ -2065,7 +2190,7 @@ function findDefaultSSHKey() {
|
|
|
2065
2190
|
return void 0;
|
|
2066
2191
|
}
|
|
2067
2192
|
function parseSSHConfig(hostAlias, configPath) {
|
|
2068
|
-
const sshConfigPath = configPath
|
|
2193
|
+
const sshConfigPath = configPath;
|
|
2069
2194
|
if (!existsSync(sshConfigPath)) {
|
|
2070
2195
|
return null;
|
|
2071
2196
|
}
|
|
@@ -2218,6 +2343,17 @@ function resolveTransport() {
|
|
|
2218
2343
|
}
|
|
2219
2344
|
return { type: "stdio", source: "default" };
|
|
2220
2345
|
}
|
|
2346
|
+
function resolveMaxRows() {
|
|
2347
|
+
const args = parseCommandLineArgs();
|
|
2348
|
+
if (args["max-rows"]) {
|
|
2349
|
+
const maxRows = parseInt(args["max-rows"], 10);
|
|
2350
|
+
if (isNaN(maxRows) || maxRows <= 0) {
|
|
2351
|
+
throw new Error(`Invalid --max-rows value: ${args["max-rows"]}. Must be a positive integer.`);
|
|
2352
|
+
}
|
|
2353
|
+
return { maxRows, source: "command line argument" };
|
|
2354
|
+
}
|
|
2355
|
+
return null;
|
|
2356
|
+
}
|
|
2221
2357
|
function resolvePort() {
|
|
2222
2358
|
const args = parseCommandLineArgs();
|
|
2223
2359
|
if (args.port) {
|
|
@@ -2260,7 +2396,9 @@ function resolveSSHConfig() {
|
|
|
2260
2396
|
sources.push("SSH_HOST from environment");
|
|
2261
2397
|
}
|
|
2262
2398
|
if (sshConfigHost && looksLikeSSHAlias(sshConfigHost)) {
|
|
2263
|
-
const
|
|
2399
|
+
const sshConfigPath = path.join(homedir2(), ".ssh", "config");
|
|
2400
|
+
console.error(`Attempting to parse SSH config for host '${sshConfigHost}' from: ${sshConfigPath}`);
|
|
2401
|
+
const sshConfigData = parseSSHConfig(sshConfigHost, sshConfigPath);
|
|
2264
2402
|
if (sshConfigData) {
|
|
2265
2403
|
config = { ...sshConfigData };
|
|
2266
2404
|
sources.push(`SSH config for host '${sshConfigHost}'`);
|
|
@@ -2327,9 +2465,15 @@ var ConnectorManager = class {
|
|
|
2327
2465
|
this.connected = false;
|
|
2328
2466
|
this.sshTunnel = null;
|
|
2329
2467
|
this.originalDSN = null;
|
|
2468
|
+
this.maxRows = null;
|
|
2330
2469
|
if (!managerInstance) {
|
|
2331
2470
|
managerInstance = this;
|
|
2332
2471
|
}
|
|
2472
|
+
const maxRowsData = resolveMaxRows();
|
|
2473
|
+
if (maxRowsData) {
|
|
2474
|
+
this.maxRows = maxRowsData.maxRows;
|
|
2475
|
+
console.error(`Max rows limit: ${this.maxRows} (from ${maxRowsData.source})`);
|
|
2476
|
+
}
|
|
2333
2477
|
}
|
|
2334
2478
|
/**
|
|
2335
2479
|
* Initialize and connect to the database using a DSN
|
|
@@ -2425,6 +2569,26 @@ var ConnectorManager = class {
|
|
|
2425
2569
|
}
|
|
2426
2570
|
return managerInstance.getConnector();
|
|
2427
2571
|
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Get execute options for SQL execution
|
|
2574
|
+
*/
|
|
2575
|
+
getExecuteOptions() {
|
|
2576
|
+
const options = {};
|
|
2577
|
+
if (this.maxRows !== null) {
|
|
2578
|
+
options.maxRows = this.maxRows;
|
|
2579
|
+
}
|
|
2580
|
+
return options;
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* Get the current execute options
|
|
2584
|
+
* This is used by tool handlers
|
|
2585
|
+
*/
|
|
2586
|
+
static getCurrentExecuteOptions() {
|
|
2587
|
+
if (!managerInstance) {
|
|
2588
|
+
throw new Error("ConnectorManager not initialized");
|
|
2589
|
+
}
|
|
2590
|
+
return managerInstance.getExecuteOptions();
|
|
2591
|
+
}
|
|
2428
2592
|
/**
|
|
2429
2593
|
* Get default port for a database based on DSN protocol
|
|
2430
2594
|
*/
|
|
@@ -2844,9 +3008,20 @@ var executeSqlSchema = {
|
|
|
2844
3008
|
function splitSQLStatements(sql2) {
|
|
2845
3009
|
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
2846
3010
|
}
|
|
3011
|
+
function stripSQLComments(sql2) {
|
|
3012
|
+
let cleaned = sql2.split("\n").map((line) => {
|
|
3013
|
+
const commentIndex = line.indexOf("--");
|
|
3014
|
+
return commentIndex >= 0 ? line.substring(0, commentIndex) : line;
|
|
3015
|
+
}).join("\n");
|
|
3016
|
+
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
3017
|
+
return cleaned.trim();
|
|
3018
|
+
}
|
|
2847
3019
|
function isReadOnlySQL(sql2, connectorType) {
|
|
2848
|
-
const
|
|
2849
|
-
|
|
3020
|
+
const cleanedSQL = stripSQLComments(sql2).toLowerCase();
|
|
3021
|
+
if (!cleanedSQL) {
|
|
3022
|
+
return true;
|
|
3023
|
+
}
|
|
3024
|
+
const firstWord = cleanedSQL.split(/\s+/)[0];
|
|
2850
3025
|
const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
|
|
2851
3026
|
return keywordList.includes(firstWord);
|
|
2852
3027
|
}
|
|
@@ -2856,6 +3031,7 @@ function areAllStatementsReadOnly(sql2, connectorType) {
|
|
|
2856
3031
|
}
|
|
2857
3032
|
async function executeSqlToolHandler({ sql: sql2 }, _extra) {
|
|
2858
3033
|
const connector = ConnectorManager.getCurrentConnector();
|
|
3034
|
+
const executeOptions = ConnectorManager.getCurrentExecuteOptions();
|
|
2859
3035
|
try {
|
|
2860
3036
|
if (isReadOnlyMode() && !areAllStatementsReadOnly(sql2, connector.id)) {
|
|
2861
3037
|
return createToolErrorResponse(
|
|
@@ -2863,7 +3039,7 @@ async function executeSqlToolHandler({ sql: sql2 }, _extra) {
|
|
|
2863
3039
|
"READONLY_VIOLATION"
|
|
2864
3040
|
);
|
|
2865
3041
|
}
|
|
2866
|
-
const result = await connector.executeSQL(sql2);
|
|
3042
|
+
const result = await connector.executeSQL(sql2, executeOptions);
|
|
2867
3043
|
const responseData = {
|
|
2868
3044
|
rows: result.rows,
|
|
2869
3045
|
count: result.rows.length
|