@bytebase/dbhub 0.22.6 → 0.24.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 +4 -0
- package/dist/{chunk-BXXHWEQS.js → chunk-D2BIPC7V.js} +10 -1
- package/dist/{chunk-RTB262PR.js → chunk-DUCNPMNW.js} +54 -1
- package/dist/chunk-JO7YQPBQ.js +130 -0
- package/dist/{chunk-WPXPKRDU.js → chunk-MQR6VX5F.js} +173 -72
- package/dist/index.js +14 -103
- package/dist/{mariadb-JPEKVFEW.js → mariadb-VZMU7LYA.js} +66 -46
- package/dist/{mysql-EK6TCER5.js → mysql-MHIZK2UY.js} +65 -46
- package/dist/{postgres-BKBCGDZS.js → postgres-ZQFXCBYA.js} +8 -2
- package/dist/{registry-IPRZ55YC.js → registry-ZU6ZFNQ2.js} +1 -1
- package/dist/{sqlite-CQ4LCBB6.js → sqlite-JH5CNV5E.js} +8 -2
- package/dist/{sqlserver-47APXMSF.js → sqlserver-LHWZ7WOP.js} +140 -5
- package/package.json +4 -1
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sqlServerDynamicSqlKeywords,
|
|
3
|
+
sqlServerDynamicSqlPattern,
|
|
4
|
+
sqlServerPassThroughKeywords,
|
|
5
|
+
sqlServerPassThroughPattern
|
|
6
|
+
} from "./chunk-JO7YQPBQ.js";
|
|
1
7
|
import {
|
|
2
8
|
isDriverNotInstalled
|
|
3
9
|
} from "./chunk-WVVMH6FJ.js";
|
|
4
10
|
import {
|
|
5
|
-
SQLRowLimiter
|
|
6
|
-
|
|
11
|
+
SQLRowLimiter,
|
|
12
|
+
closeQuietly
|
|
13
|
+
} from "./chunk-D2BIPC7V.js";
|
|
7
14
|
import {
|
|
8
15
|
ConnectorRegistry,
|
|
9
16
|
SafeURL,
|
|
@@ -156,8 +163,14 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
156
163
|
if (!this.config.options) {
|
|
157
164
|
this.config.options = {};
|
|
158
165
|
}
|
|
159
|
-
this.connection =
|
|
166
|
+
this.connection = new sql.ConnectionPool(this.config);
|
|
167
|
+
await this.connection.connect();
|
|
160
168
|
} catch (error) {
|
|
169
|
+
if (this.connection) {
|
|
170
|
+
const connection = this.connection;
|
|
171
|
+
this.connection = void 0;
|
|
172
|
+
await closeQuietly(() => connection.close());
|
|
173
|
+
}
|
|
161
174
|
throw error;
|
|
162
175
|
}
|
|
163
176
|
}
|
|
@@ -462,13 +475,16 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
462
475
|
sqlQuery.match(_SQLServerConnector.LEADING_NOISE)[0].length
|
|
463
476
|
);
|
|
464
477
|
if (/^explain\b/i.test(afterNoise)) {
|
|
465
|
-
return this.explainQuery(afterNoise.slice("explain".length).trim());
|
|
478
|
+
return this.explainQuery(afterNoise.slice("explain".length).trim(), options.readonly);
|
|
466
479
|
}
|
|
467
480
|
try {
|
|
468
481
|
let processedSQL = sqlQuery;
|
|
469
482
|
if (options.maxRows) {
|
|
470
483
|
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
|
|
471
484
|
}
|
|
485
|
+
if (options.readonly) {
|
|
486
|
+
return await this.executeReadOnly(processedSQL, parameters);
|
|
487
|
+
}
|
|
472
488
|
const request = this.connection.request();
|
|
473
489
|
const messages = [];
|
|
474
490
|
request.on(
|
|
@@ -525,6 +541,122 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
525
541
|
throw new Error(`Failed to execute query: ${error.message}`);
|
|
526
542
|
}
|
|
527
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* Reject the constructs that escape SQL Server's read-only guards, for use by
|
|
546
|
+
* both read-only execution paths.
|
|
547
|
+
*
|
|
548
|
+
* - Dynamic SQL (sqlServerDynamicSqlKeywords): can carry hidden COMMIT/ROLLBACK
|
|
549
|
+
* inside string literals that stripCommentsAndStrings removes
|
|
550
|
+
* - Pass-through data sources (sqlServerPassThroughKeywords): execute on a
|
|
551
|
+
* remote or ad-hoc source, so a local rollback never reaches them
|
|
552
|
+
* - COMMIT/ROLLBACK, when `transactionControl` is set: would end the wrapping
|
|
553
|
+
* transaction, letting writes persist. Only meaningful for the transaction
|
|
554
|
+
* path; the EXPLAIN path opens no transaction of its own.
|
|
555
|
+
*
|
|
556
|
+
* Both keyword lists are imported from the read-only classifier rather than
|
|
557
|
+
* redeclared, so the classifier and these backstops cannot drift apart.
|
|
558
|
+
*
|
|
559
|
+
* Note the COMMIT/ROLLBACK check is SQL Server-only by design. MySQL/MariaDB
|
|
560
|
+
* wrap batches in a transaction too, but there `commit`, `prepare` and
|
|
561
|
+
* `execute` are absent from their allow-lists in allowedKeywords, and
|
|
562
|
+
* execute-sql.ts requires every split statement to pass the classifier — so a
|
|
563
|
+
* transaction-control statement can never reach their backstop.
|
|
564
|
+
*/
|
|
565
|
+
assertNoReadOnlyEscapes(sqlText, { transactionControl = false } = {}) {
|
|
566
|
+
const cleaned = stripCommentsAndStrings(sqlText, "sqlserver").toLowerCase();
|
|
567
|
+
if (transactionControl && /\b(?:commit|rollback)\b/.test(cleaned)) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
"Read-only mode: transaction control statements (COMMIT, ROLLBACK) are not allowed"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (sqlServerDynamicSqlPattern.test(cleaned)) {
|
|
573
|
+
throw new Error(
|
|
574
|
+
`Read-only mode: dynamic SQL execution (${sqlServerDynamicSqlKeywords.map((k) => k.toUpperCase()).join(", ")}) is not allowed`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
if (sqlServerPassThroughPattern.test(cleaned)) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
`Read-only mode: pass-through data sources (${sqlServerPassThroughKeywords.map((k) => k.toUpperCase()).join(", ")}) are not allowed`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Execute a query inside a transaction that always rolls back, preventing
|
|
585
|
+
* any modifications from persisting. SQL Server has no native READ ONLY
|
|
586
|
+
* transaction mode, so this is the defense-in-depth backstop behind the
|
|
587
|
+
* keyword classifier.
|
|
588
|
+
*
|
|
589
|
+
* Dangerous constructs are rejected before the transaction opens; see
|
|
590
|
+
* assertNoReadOnlyEscapes.
|
|
591
|
+
*/
|
|
592
|
+
async executeReadOnly(processedSQL, parameters) {
|
|
593
|
+
this.assertNoReadOnlyEscapes(processedSQL, { transactionControl: true });
|
|
594
|
+
const transaction = new sql.Transaction(this.connection);
|
|
595
|
+
await transaction.begin();
|
|
596
|
+
const request = new sql.Request(transaction);
|
|
597
|
+
const messages = [];
|
|
598
|
+
request.on(
|
|
599
|
+
"info",
|
|
600
|
+
(info) => {
|
|
601
|
+
messages.push({
|
|
602
|
+
text: info.message,
|
|
603
|
+
severity: info.class !== void 0 ? String(info.class) : void 0,
|
|
604
|
+
code: info.number,
|
|
605
|
+
line: info.lineNumber
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
if (parameters && parameters.length > 0) {
|
|
610
|
+
parameters.forEach((param, index) => {
|
|
611
|
+
const paramName = `p${index + 1}`;
|
|
612
|
+
if (typeof param === "string") {
|
|
613
|
+
request.input(paramName, sql.VarChar, param);
|
|
614
|
+
} else if (typeof param === "number") {
|
|
615
|
+
if (Number.isInteger(param)) {
|
|
616
|
+
request.input(paramName, sql.Int, param);
|
|
617
|
+
} else {
|
|
618
|
+
request.input(paramName, sql.Float, param);
|
|
619
|
+
}
|
|
620
|
+
} else if (typeof param === "boolean") {
|
|
621
|
+
request.input(paramName, sql.Bit, param);
|
|
622
|
+
} else if (param === null || param === void 0) {
|
|
623
|
+
request.input(paramName, sql.VarChar, param);
|
|
624
|
+
} else if (Array.isArray(param)) {
|
|
625
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
626
|
+
} else {
|
|
627
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
let result;
|
|
632
|
+
let queryFailed = false;
|
|
633
|
+
try {
|
|
634
|
+
result = await request.query(processedSQL);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
queryFailed = true;
|
|
637
|
+
console.error(`[SQL Server executeReadOnly] ERROR: ${error.message}`);
|
|
638
|
+
console.error(`[SQL Server executeReadOnly] SQL: ${processedSQL}`);
|
|
639
|
+
if (parameters && parameters.length > 0) {
|
|
640
|
+
console.error(`[SQL Server executeReadOnly] Parameters: ${JSON.stringify(parameters)}`);
|
|
641
|
+
}
|
|
642
|
+
throw error;
|
|
643
|
+
} finally {
|
|
644
|
+
try {
|
|
645
|
+
await transaction.rollback();
|
|
646
|
+
} catch (rollbackError) {
|
|
647
|
+
if (!queryFailed) {
|
|
648
|
+
throw new Error(
|
|
649
|
+
`Read-only rollback failed \u2014 data may have been modified: ${rollbackError.message}`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
rows: result.recordset || [],
|
|
656
|
+
rowCount: result.rowsAffected[0] || 0,
|
|
657
|
+
...messages.length > 0 ? { messages } : {}
|
|
658
|
+
};
|
|
659
|
+
}
|
|
528
660
|
/**
|
|
529
661
|
* Return the estimated execution plan for a query using SHOWPLAN_XML.
|
|
530
662
|
*
|
|
@@ -539,11 +671,14 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
539
671
|
* off the shared pool, so a concurrent query can never land on a connection
|
|
540
672
|
* with SHOWPLAN enabled (which would return a plan instead of its results).
|
|
541
673
|
*/
|
|
542
|
-
async explainQuery(innerQuery) {
|
|
674
|
+
async explainQuery(innerQuery, readonly) {
|
|
543
675
|
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
544
676
|
if (!cleaned) {
|
|
545
677
|
throw new Error("EXPLAIN requires a statement to analyze");
|
|
546
678
|
}
|
|
679
|
+
if (readonly) {
|
|
680
|
+
this.assertNoReadOnlyEscapes(innerQuery);
|
|
681
|
+
}
|
|
547
682
|
if (/\bset\s+showplan/i.test(cleaned)) {
|
|
548
683
|
throw new Error("EXPLAIN does not support SET SHOWPLAN statements");
|
|
549
684
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"mcpName": "io.github.bytebase/dbhub",
|
|
5
5
|
"description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
|
|
6
6
|
"repository": {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"build": "pnpm run generate:api-types && tsup && cd frontend && pnpm run build",
|
|
22
22
|
"build:backend": "pnpm run generate:api-types && tsup",
|
|
23
23
|
"build:frontend": "cd frontend && pnpm run build",
|
|
24
|
+
"build:mcpb": "pnpm run build:backend && node scripts/build-mcpb.mjs",
|
|
25
|
+
"test:mcpb": "node scripts/smoke-test-mcpb.mjs",
|
|
24
26
|
"start": "node dist/index.js",
|
|
25
27
|
"dev": "concurrently --kill-others \"pnpm run dev:backend\" \"pnpm run dev:frontend\"",
|
|
26
28
|
"dev:backend": "NODE_ENV=development tsx src/index.ts --transport=http",
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
"pg": "^8.13.3"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
63
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
61
64
|
"@testcontainers/mariadb": "^11.0.3",
|
|
62
65
|
"@testcontainers/mssqlserver": "^11.0.3",
|
|
63
66
|
"@testcontainers/mysql": "^11.0.3",
|