@bytebase/dbhub 1.2.0 → 1.2.1
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 +31 -2
- package/dist/{chunk-CUBIR72I.js → chunk-2DFLBX4D.js} +22 -4
- package/dist/{chunk-RSB7DPO2.js → chunk-6QETXS44.js} +56 -1
- package/dist/{chunk-QEFMUQJ2.js → chunk-A4TSDCPY.js} +1 -1
- package/dist/{chunk-PBN2UXB5.js → chunk-IQRBLAB4.js} +1 -1
- package/dist/{chunk-WIQ4DWGY.js → chunk-MF6LSVWK.js} +3 -1
- package/dist/index.js +15 -11
- package/dist/{mariadb-E33EXGJZ.js → mariadb-DQ6P57IX.js} +13 -6
- package/dist/{mysql-RLDQI3SJ.js → mysql-GXDUNLJ6.js} +13 -6
- package/dist/{postgres-6XY2GPQV.js → postgres-2FHS64EO.js} +26 -14
- package/dist/{registry-XMEJF7WW.js → registry-D2QVU6AG.js} +2 -2
- package/dist/{sqlite-CZE6Z36Y.js → sqlite-ASYMU5BI.js} +20 -12
- package/dist/{sqlserver-IIGRXGYK.js → sqlserver-WNVSBFJ3.js} +36 -16
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
> [!NOTE]
|
|
2
2
|
> Brought to you by [Bytebase](https://www.bytebase.com/), open-source database governance platform.
|
|
3
3
|
|
|
4
|
+
<p align="center">
|
|
5
|
+
<a href="https://www.star-history.com/bytebase/dbhub">
|
|
6
|
+
<picture>
|
|
7
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=bytebase/dbhub&type=trending&theme=dark" />
|
|
8
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=bytebase/dbhub&type=trending" />
|
|
9
|
+
<img alt="GitHub Trending Repository of the Day" src="https://api.star-history.com/badge?repo=bytebase/dbhub&type=trending" />
|
|
10
|
+
</picture>
|
|
11
|
+
</a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
4
14
|
<p align="center">
|
|
5
15
|
<a href="https://dbhub.ai/" target="_blank">
|
|
6
16
|
<picture>
|
|
@@ -30,14 +40,33 @@
|
|
|
30
40
|
MCP Clients MCP Server Databases
|
|
31
41
|
```
|
|
32
42
|
|
|
33
|
-
DBHub is a token
|
|
43
|
+
DBHub is a minimal MCP server: token-efficient, zero-dependency, and just two tools by default with opt-in extras. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
|
|
34
44
|
|
|
35
|
-
- **
|
|
45
|
+
- **Minimal**: Zero dependency, token efficient with a minimal set of MCP tools to maximize context window
|
|
36
46
|
- **Multi-Database**: PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite through a single interface
|
|
37
47
|
- **Multi-Connection**: Connect to multiple databases simultaneously with TOML configuration
|
|
38
48
|
- **Guardrails**: Read-only mode, row limiting, and query timeout to prevent runaway operations
|
|
39
49
|
- **Secure Access**: SSH tunneling and SSL/TLS encryption
|
|
40
50
|
|
|
51
|
+
> DBHub is the official example in the [Claude Code docs](https://code.claude.com/docs/en/mcp#example-query-your-postgresql-database) for connecting to PostgreSQL via MCP.
|
|
52
|
+
|
|
53
|
+
## Token Efficiency
|
|
54
|
+
|
|
55
|
+
DBHub loads just 2 tools by default at **1.4k tokens** — 13-14x fewer than alternatives — keeping the context window open for your actual work.
|
|
56
|
+
|
|
57
|
+
| MCP Server | Default Config | Default Tools |
|
|
58
|
+
|------------|---------------|--------------|
|
|
59
|
+
| **DBHub** | **1.4k** | 2 (`execute_sql`, `search_objects`) |
|
|
60
|
+
| MCP Toolbox | 19.0k | 28 |
|
|
61
|
+
| Supabase MCP | 19.3k | all |
|
|
62
|
+
|
|
63
|
+
## Use Cases
|
|
64
|
+
|
|
65
|
+
- **Local Development**: Schema exploration, query validation, and data debugging with Claude Code, VS Code, Cursor, etc.
|
|
66
|
+
- **Non-Technical Access**: Expose curated, read-only views to non-technical staff via Claude Desktop, VS Code, Cursor, etc.
|
|
67
|
+
- **Multi-Database Consolidation**: Replace separate MCP servers for each database with a single DBHub process
|
|
68
|
+
- **Production Troubleshooting**: Read-only diagnostics with guardrails against runaway queries
|
|
69
|
+
|
|
41
70
|
## Supported Databases
|
|
42
71
|
|
|
43
72
|
PostgreSQL, MySQL, SQL Server, MariaDB, and SQLite.
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getDefaultPortForType,
|
|
9
9
|
parseConnectionInfoFromDSN,
|
|
10
10
|
stripCommentsAndStrings
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-MF6LSVWK.js";
|
|
12
12
|
|
|
13
13
|
// src/tools/builtin-tools.ts
|
|
14
14
|
var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
|
|
@@ -1307,6 +1307,18 @@ function validateSourceConfig(source, configPath) {
|
|
|
1307
1307
|
);
|
|
1308
1308
|
}
|
|
1309
1309
|
}
|
|
1310
|
+
if (source.aws_profile !== void 0) {
|
|
1311
|
+
if (typeof source.aws_profile !== "string" || source.aws_profile.trim().length === 0) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid aws_profile. Must be a non-empty string.`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
if (source.aws_iam_auth !== true) {
|
|
1317
|
+
throw new Error(
|
|
1318
|
+
`Configuration file ${configPath}: source '${source.id}' aws_profile requires aws_iam_auth = true.`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1310
1322
|
if (source.aws_iam_auth === true) {
|
|
1311
1323
|
const validIamTypes = ["postgres", "mysql", "mariadb"];
|
|
1312
1324
|
if (!source.type || !validIamTypes.includes(source.type)) {
|
|
@@ -1677,12 +1689,17 @@ async function generateRdsAuthToken(params) {
|
|
|
1677
1689
|
}
|
|
1678
1690
|
throw error;
|
|
1679
1691
|
}
|
|
1680
|
-
const
|
|
1692
|
+
const signerConfig = {
|
|
1681
1693
|
hostname: params.hostname,
|
|
1682
1694
|
port: params.port,
|
|
1683
1695
|
username: params.username,
|
|
1684
1696
|
region: params.region
|
|
1685
|
-
}
|
|
1697
|
+
};
|
|
1698
|
+
if (params.profile) {
|
|
1699
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
1700
|
+
signerConfig.credentials = fromIni({ profile: params.profile });
|
|
1701
|
+
}
|
|
1702
|
+
const signer = new Signer(signerConfig);
|
|
1686
1703
|
return signer.getAuthToken();
|
|
1687
1704
|
}
|
|
1688
1705
|
|
|
@@ -2158,7 +2175,8 @@ var ConnectorManager = class {
|
|
|
2158
2175
|
hostname,
|
|
2159
2176
|
port,
|
|
2160
2177
|
username,
|
|
2161
|
-
region: source.aws_region
|
|
2178
|
+
region: source.aws_region,
|
|
2179
|
+
profile: source.aws_profile
|
|
2162
2180
|
});
|
|
2163
2181
|
const queryParams = new Map(parsed.searchParams);
|
|
2164
2182
|
const currentSslMode = queryParams.get("sslmode");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
blankCommentsAndStrings,
|
|
3
3
|
stripCommentsAndStrings
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MF6LSVWK.js";
|
|
5
5
|
|
|
6
6
|
// src/utils/sql-row-limiter.ts
|
|
7
7
|
var SQLRowLimiter = class {
|
|
@@ -183,6 +183,61 @@ LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
|
183
183
|
}
|
|
184
184
|
return this.applyTopToQuery(sql, maxRows);
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Like applyMaxRows, but requests one probe row (maxRows + 1) whenever the
|
|
188
|
+
* cap is the binding constraint, so callers can detect truncation exactly
|
|
189
|
+
* instead of guessing from `rowCount === maxRows` (which a table with
|
|
190
|
+
* exactly maxRows rows also produces).
|
|
191
|
+
*
|
|
192
|
+
* `probeApplied` is false when the cap cannot fire: no maxRows, not a
|
|
193
|
+
* SELECT, or the query's own literal LIMIT is already within the cap (the
|
|
194
|
+
* user asked for fewer rows than the cap — that is their limit, not
|
|
195
|
+
* truncation).
|
|
196
|
+
*/
|
|
197
|
+
static applyMaxRowsWithTruncationProbe(sql, maxRows) {
|
|
198
|
+
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
199
|
+
return { sql, probeApplied: false };
|
|
200
|
+
}
|
|
201
|
+
if (!this.hasParameterizedLimit(sql)) {
|
|
202
|
+
const existingLimit = this.extractLimitValue(sql);
|
|
203
|
+
if (existingLimit !== null && existingLimit <= maxRows) {
|
|
204
|
+
return { sql, probeApplied: false };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { sql: this.applyMaxRows(sql, maxRows + 1), probeApplied: true };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* SQL Server variant of applyMaxRowsWithTruncationProbe, using TOP syntax.
|
|
211
|
+
* A set-operator statement is always wrapped (any TOP on an individual
|
|
212
|
+
* branch caps only that branch, not the combined output), so the probe
|
|
213
|
+
* applies there regardless of branch-level TOPs.
|
|
214
|
+
*/
|
|
215
|
+
static applyMaxRowsForSQLServerWithTruncationProbe(sql, maxRows) {
|
|
216
|
+
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
217
|
+
return { sql, probeApplied: false };
|
|
218
|
+
}
|
|
219
|
+
if (!this.hasSetOperator(sql)) {
|
|
220
|
+
const existingTop = this.extractTopValue(sql);
|
|
221
|
+
if (existingTop !== null && existingTop <= maxRows) {
|
|
222
|
+
return { sql, probeApplied: false };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { sql: this.applyMaxRowsForSQLServer(sql, maxRows + 1), probeApplied: true };
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Post-process a result set produced by a truncation-probe rewrite: when
|
|
229
|
+
* the probe row came back (more than maxRows rows), drop it, clamp the
|
|
230
|
+
* count to maxRows, and mark the set truncated so consumers can tell a
|
|
231
|
+
* capped result from a complete one.
|
|
232
|
+
*/
|
|
233
|
+
static flagTruncation(resultSet, maxRows, probeApplied) {
|
|
234
|
+
if (!probeApplied || !maxRows || resultSet.rows.length <= maxRows) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
resultSet.rows = resultSet.rows.slice(0, maxRows);
|
|
238
|
+
resultSet.rowCount = maxRows;
|
|
239
|
+
resultSet.truncated = true;
|
|
240
|
+
}
|
|
186
241
|
};
|
|
187
242
|
|
|
188
243
|
// src/utils/resource-cleanup.ts
|
|
@@ -91,7 +91,9 @@ var SafeURL = class {
|
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
-
const
|
|
94
|
+
const pathStart = urlString.indexOf("/");
|
|
95
|
+
const authorityEnd = pathStart === -1 ? urlString.length : pathStart;
|
|
96
|
+
const atIndex = urlString.lastIndexOf("@", authorityEnd - 1);
|
|
95
97
|
if (atIndex !== -1) {
|
|
96
98
|
const auth = urlString.substring(0, atIndex);
|
|
97
99
|
urlString = urlString.substring(atIndex + 1);
|
package/dist/index.js
CHANGED
|
@@ -18,14 +18,14 @@ import {
|
|
|
18
18
|
resolveSourceConfigs,
|
|
19
19
|
resolveTomlConfigPath,
|
|
20
20
|
resolveTransport
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-2DFLBX4D.js";
|
|
22
22
|
import {
|
|
23
23
|
allowedKeywords,
|
|
24
24
|
getFirstKeyword,
|
|
25
25
|
hasEscapeHatchFunction,
|
|
26
26
|
isReadOnlySQL,
|
|
27
27
|
sqlServerDynamicSqlPattern
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-IQRBLAB4.js";
|
|
29
29
|
import {
|
|
30
30
|
loadConnectors
|
|
31
31
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
parseConnectionInfoFromDSN,
|
|
40
40
|
splitSQLStatements,
|
|
41
41
|
stripCommentsAndStrings
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-MF6LSVWK.js";
|
|
43
43
|
|
|
44
44
|
// src/server.ts
|
|
45
45
|
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
|
|
@@ -224,7 +224,11 @@ function toStatementsPayload(resultSets) {
|
|
|
224
224
|
return resultSets.map((set) => ({
|
|
225
225
|
sql: set.sql,
|
|
226
226
|
rows: set.rows,
|
|
227
|
-
count: set.rowCount
|
|
227
|
+
count: set.rowCount,
|
|
228
|
+
// Only present when a max_rows cap actually cut off rows, so consumers
|
|
229
|
+
// can tell a capped result from a table with exactly max_rows rows
|
|
230
|
+
// (and complete results don't pay the extra tokens).
|
|
231
|
+
...set.truncated ? { truncated: true } : {}
|
|
228
232
|
}));
|
|
229
233
|
}
|
|
230
234
|
function resolveTrackedToolName(sourceId, baseName) {
|
|
@@ -1008,7 +1012,7 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
1008
1012
|
maxRows: toolConfig?.max_rows
|
|
1009
1013
|
};
|
|
1010
1014
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
1011
|
-
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
1015
|
+
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows; capped results carry "truncated": true)` : "";
|
|
1012
1016
|
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
1013
1017
|
const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
|
|
1014
1018
|
const annotations = {
|
|
@@ -1909,7 +1913,7 @@ See documentation for more details on configuring database connections.
|
|
|
1909
1913
|
const sources = sourceConfigsData.sources;
|
|
1910
1914
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1911
1915
|
await connectorManager.connectWithSources(sources);
|
|
1912
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1916
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-D2QVU6AG.js");
|
|
1913
1917
|
initializeToolRegistry2({
|
|
1914
1918
|
sources: sourceConfigsData.sources,
|
|
1915
1919
|
tools: sourceConfigsData.tools
|
|
@@ -2064,11 +2068,11 @@ See documentation for more details on configuring database connections.
|
|
|
2064
2068
|
|
|
2065
2069
|
// src/index.ts
|
|
2066
2070
|
var connectorModules = [
|
|
2067
|
-
{ load: () => import("./postgres-
|
|
2068
|
-
{ load: () => import("./sqlserver-
|
|
2069
|
-
{ load: () => import("./sqlite-
|
|
2070
|
-
{ load: () => import("./mysql-
|
|
2071
|
-
{ load: () => import("./mariadb-
|
|
2071
|
+
{ load: () => import("./postgres-2FHS64EO.js"), name: "PostgreSQL", driver: "pg" },
|
|
2072
|
+
{ load: () => import("./sqlserver-WNVSBFJ3.js"), name: "SQL Server", driver: "mssql" },
|
|
2073
|
+
{ load: () => import("./sqlite-ASYMU5BI.js"), name: "SQLite", driver: "node:sqlite" },
|
|
2074
|
+
{ load: () => import("./mysql-GXDUNLJ6.js"), name: "MySQL", driver: "mysql2" },
|
|
2075
|
+
{ load: () => import("./mariadb-DQ6P57IX.js"), name: "MariaDB", driver: "mariadb" }
|
|
2072
2076
|
];
|
|
2073
2077
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
2074
2078
|
console.error("Fatal error:", error);
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
parseQueryResultSets,
|
|
6
6
|
requireDatabaseInDSN,
|
|
7
7
|
withReadOnlyTransaction
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-A4TSDCPY.js";
|
|
9
9
|
import "./chunk-FU2ZJE4E.js";
|
|
10
10
|
import {
|
|
11
11
|
quoteIdentifier
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
SQLRowLimiter,
|
|
15
15
|
closeQuietly
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-6QETXS44.js";
|
|
17
17
|
import {
|
|
18
18
|
ConnectorRegistry,
|
|
19
19
|
SafeURL,
|
|
20
20
|
obfuscateDSNPassword,
|
|
21
21
|
splitSQLStatements
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-MF6LSVWK.js";
|
|
23
23
|
|
|
24
24
|
// src/connectors/mariadb/index.ts
|
|
25
25
|
import * as mariadb from "mariadb";
|
|
@@ -506,11 +506,13 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
506
506
|
async () => {
|
|
507
507
|
const statements = splitSQLStatements(sql, "mariadb");
|
|
508
508
|
let processedSQL = sql;
|
|
509
|
+
let probes = [];
|
|
509
510
|
if (options.maxRows) {
|
|
510
|
-
const
|
|
511
|
-
(statement) => SQLRowLimiter.
|
|
511
|
+
const rewrites = statements.map(
|
|
512
|
+
(statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows)
|
|
512
513
|
);
|
|
513
|
-
|
|
514
|
+
probes = rewrites.map((rewrite) => rewrite.probeApplied);
|
|
515
|
+
processedSQL = rewrites.map((rewrite) => rewrite.sql).join("; ");
|
|
514
516
|
if (sql.trim().endsWith(";")) {
|
|
515
517
|
processedSQL += ";";
|
|
516
518
|
}
|
|
@@ -522,6 +524,11 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
522
524
|
results = await conn.query(processedSQL);
|
|
523
525
|
}
|
|
524
526
|
const resultSets = parseQueryResultSets(results, statements);
|
|
527
|
+
if (resultSets.length === probes.length) {
|
|
528
|
+
resultSets.forEach(
|
|
529
|
+
(set, index) => SQLRowLimiter.flagTruncation(set, options.maxRows, probes[index])
|
|
530
|
+
);
|
|
531
|
+
}
|
|
525
532
|
return { resultSets };
|
|
526
533
|
}
|
|
527
534
|
);
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
parseQueryResultSets,
|
|
7
7
|
requireDatabaseInDSN,
|
|
8
8
|
withReadOnlyTransaction
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-A4TSDCPY.js";
|
|
10
10
|
import "./chunk-FU2ZJE4E.js";
|
|
11
11
|
import {
|
|
12
12
|
quoteIdentifier
|
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
SQLRowLimiter,
|
|
16
16
|
closeQuietly
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-6QETXS44.js";
|
|
18
18
|
import {
|
|
19
19
|
ConnectorRegistry,
|
|
20
20
|
SafeURL,
|
|
21
21
|
obfuscateDSNPassword,
|
|
22
22
|
splitSQLStatements
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-MF6LSVWK.js";
|
|
24
24
|
|
|
25
25
|
// src/connectors/mysql/index.ts
|
|
26
26
|
import mysql from "mysql2/promise";
|
|
@@ -517,11 +517,13 @@ var _MySQLConnector = class _MySQLConnector {
|
|
|
517
517
|
async () => {
|
|
518
518
|
const statements = splitSQLStatements(sql, "mysql");
|
|
519
519
|
let processedSQL = sql;
|
|
520
|
+
let probes = [];
|
|
520
521
|
if (options.maxRows) {
|
|
521
|
-
const
|
|
522
|
-
(statement) => SQLRowLimiter.
|
|
522
|
+
const rewrites = statements.map(
|
|
523
|
+
(statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows)
|
|
523
524
|
);
|
|
524
|
-
|
|
525
|
+
probes = rewrites.map((rewrite) => rewrite.probeApplied);
|
|
526
|
+
processedSQL = rewrites.map((rewrite) => rewrite.sql).join("; ");
|
|
525
527
|
if (sql.trim().endsWith(";")) {
|
|
526
528
|
processedSQL += ";";
|
|
527
529
|
}
|
|
@@ -534,6 +536,11 @@ var _MySQLConnector = class _MySQLConnector {
|
|
|
534
536
|
}
|
|
535
537
|
const [firstResult] = results;
|
|
536
538
|
const resultSets = parseQueryResultSets(firstResult, statements);
|
|
539
|
+
if (resultSets.length === probes.length) {
|
|
540
|
+
resultSets.forEach(
|
|
541
|
+
(set, index) => SQLRowLimiter.flagTruncation(set, options.maxRows, probes[index])
|
|
542
|
+
);
|
|
543
|
+
}
|
|
537
544
|
return { resultSets };
|
|
538
545
|
}
|
|
539
546
|
);
|
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
SQLRowLimiter,
|
|
9
9
|
closeQuietly
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-6QETXS44.js";
|
|
11
11
|
import {
|
|
12
12
|
ConnectorRegistry,
|
|
13
13
|
SafeURL,
|
|
14
14
|
obfuscateDSNPassword,
|
|
15
15
|
splitSQLStatements
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-MF6LSVWK.js";
|
|
17
17
|
|
|
18
18
|
// src/connectors/postgres/index.ts
|
|
19
19
|
import fs from "fs";
|
|
@@ -557,17 +557,22 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
557
557
|
try {
|
|
558
558
|
const statements = splitSQLStatements(sql, "postgres");
|
|
559
559
|
if (statements.length === 1) {
|
|
560
|
-
const processedStatement = SQLRowLimiter.
|
|
560
|
+
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
561
|
+
statements[0],
|
|
562
|
+
options.maxRows
|
|
563
|
+
);
|
|
561
564
|
if (options.readonly) {
|
|
562
565
|
await client.query("BEGIN READ ONLY");
|
|
563
566
|
try {
|
|
564
567
|
const result2 = parameters && parameters.length > 0 ? await client.query(processedStatement, parameters) : await client.query(processedStatement);
|
|
565
568
|
await client.query("COMMIT");
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
569
|
+
const resultSet2 = {
|
|
570
|
+
sql: statements[0],
|
|
571
|
+
rows: result2.rows,
|
|
572
|
+
rowCount: result2.rowCount ?? result2.rows.length
|
|
570
573
|
};
|
|
574
|
+
SQLRowLimiter.flagTruncation(resultSet2, options.maxRows, probeApplied);
|
|
575
|
+
return { resultSets: [resultSet2] };
|
|
571
576
|
} catch (error) {
|
|
572
577
|
try {
|
|
573
578
|
await client.query("ROLLBACK");
|
|
@@ -582,11 +587,13 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
582
587
|
} else {
|
|
583
588
|
result = await client.query(processedStatement);
|
|
584
589
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
590
|
+
const resultSet = {
|
|
591
|
+
sql: statements[0],
|
|
592
|
+
rows: result.rows,
|
|
593
|
+
rowCount: result.rowCount ?? result.rows.length
|
|
589
594
|
};
|
|
595
|
+
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
|
|
596
|
+
return { resultSets: [resultSet] };
|
|
590
597
|
} else {
|
|
591
598
|
if (parameters && parameters.length > 0) {
|
|
592
599
|
throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
|
|
@@ -595,13 +602,18 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
595
602
|
await client.query(options.readonly ? "BEGIN READ ONLY" : "BEGIN");
|
|
596
603
|
try {
|
|
597
604
|
for (let statement of statements) {
|
|
598
|
-
const processedStatement = SQLRowLimiter.
|
|
605
|
+
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
606
|
+
statement,
|
|
607
|
+
options.maxRows
|
|
608
|
+
);
|
|
599
609
|
const result = await client.query(processedStatement);
|
|
600
|
-
|
|
610
|
+
const resultSet = {
|
|
601
611
|
sql: statement,
|
|
602
612
|
rows: result.rows ?? [],
|
|
603
613
|
rowCount: result.rowCount ?? result.rows?.length ?? 0
|
|
604
|
-
}
|
|
614
|
+
};
|
|
615
|
+
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
|
|
616
|
+
resultSets.push(resultSet);
|
|
605
617
|
}
|
|
606
618
|
await client.query("COMMIT");
|
|
607
619
|
} catch (error) {
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
ToolRegistry,
|
|
3
3
|
getToolRegistry,
|
|
4
4
|
initializeToolRegistry
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-2DFLBX4D.js";
|
|
6
6
|
import "./chunk-WVVMH6FJ.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-MF6LSVWK.js";
|
|
8
8
|
export {
|
|
9
9
|
ToolRegistry,
|
|
10
10
|
getToolRegistry,
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
SQLRowLimiter,
|
|
6
6
|
closeQuietly
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-6QETXS44.js";
|
|
8
8
|
import {
|
|
9
9
|
ConnectorRegistry,
|
|
10
10
|
SafeURL,
|
|
11
11
|
obfuscateDSNPassword,
|
|
12
12
|
splitSQLStatements
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MF6LSVWK.js";
|
|
14
14
|
|
|
15
15
|
// src/connectors/sqlite/suppress-experimental-warning.ts
|
|
16
16
|
var installed = false;
|
|
@@ -334,17 +334,20 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
334
334
|
let processedStatement = statements[0];
|
|
335
335
|
const trimmedStatement = statements[0].toLowerCase().trim();
|
|
336
336
|
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"));
|
|
337
|
+
let probeApplied = false;
|
|
337
338
|
if (options.maxRows) {
|
|
338
|
-
|
|
339
|
+
const rewrite = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
340
|
+
processedStatement,
|
|
341
|
+
options.maxRows
|
|
342
|
+
);
|
|
343
|
+
processedStatement = rewrite.sql;
|
|
344
|
+
probeApplied = rewrite.probeApplied;
|
|
339
345
|
}
|
|
340
346
|
if (isReadStatement) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const rows = this.prepare(processedStatement).all();
|
|
346
|
-
return { resultSets: [{ sql: statements[0], rows, rowCount: rows.length }] };
|
|
347
|
-
}
|
|
347
|
+
const rows = parameters && parameters.length > 0 ? this.prepare(processedStatement).all(...parameters) : this.prepare(processedStatement).all();
|
|
348
|
+
const resultSet = { sql: statements[0], rows, rowCount: rows.length };
|
|
349
|
+
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
|
|
350
|
+
return { resultSets: [resultSet] };
|
|
348
351
|
} else {
|
|
349
352
|
let result;
|
|
350
353
|
if (parameters && parameters.length > 0) {
|
|
@@ -382,9 +385,14 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
382
385
|
if (options.readonly) {
|
|
383
386
|
this.db.exec("PRAGMA query_only = ON");
|
|
384
387
|
}
|
|
385
|
-
const processedStatement = SQLRowLimiter.
|
|
388
|
+
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
389
|
+
statement,
|
|
390
|
+
options.maxRows
|
|
391
|
+
);
|
|
386
392
|
const rows = this.prepare(processedStatement).all();
|
|
387
|
-
|
|
393
|
+
const resultSet = { sql: statement, rows, rowCount: rows.length };
|
|
394
|
+
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
|
|
395
|
+
resultSets.push(resultSet);
|
|
388
396
|
}
|
|
389
397
|
return { resultSets };
|
|
390
398
|
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
sqlServerDynamicSqlPattern,
|
|
4
4
|
sqlServerPassThroughKeywords,
|
|
5
5
|
sqlServerPassThroughPattern
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-IQRBLAB4.js";
|
|
7
7
|
import {
|
|
8
8
|
isDriverNotInstalled
|
|
9
9
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -14,14 +14,14 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
SQLRowLimiter,
|
|
16
16
|
closeQuietly
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-6QETXS44.js";
|
|
18
18
|
import {
|
|
19
19
|
ConnectorRegistry,
|
|
20
20
|
SafeURL,
|
|
21
21
|
obfuscateDSNPassword,
|
|
22
22
|
splitSQLStatements,
|
|
23
23
|
stripCommentsAndStrings
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-MF6LSVWK.js";
|
|
25
25
|
|
|
26
26
|
// src/connectors/sqlserver/index.ts
|
|
27
27
|
import sql from "mssql";
|
|
@@ -562,12 +562,24 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
562
562
|
}
|
|
563
563
|
try {
|
|
564
564
|
let processedSQL = sqlQuery;
|
|
565
|
+
let probeApplied = false;
|
|
565
566
|
if (options.maxRows) {
|
|
566
|
-
|
|
567
|
+
const rewrite = SQLRowLimiter.applyMaxRowsForSQLServerWithTruncationProbe(
|
|
568
|
+
sqlQuery,
|
|
569
|
+
options.maxRows
|
|
570
|
+
);
|
|
571
|
+
processedSQL = rewrite.sql;
|
|
572
|
+
probeApplied = rewrite.probeApplied;
|
|
567
573
|
}
|
|
568
574
|
const isSingleStatement = splitSQLStatements(processedSQL, "sqlserver").length === 1;
|
|
569
575
|
if (options.readonly) {
|
|
570
|
-
return await this.executeReadOnly(
|
|
576
|
+
return await this.executeReadOnly(
|
|
577
|
+
processedSQL,
|
|
578
|
+
parameters,
|
|
579
|
+
isSingleStatement ? sqlQuery : void 0,
|
|
580
|
+
options.maxRows,
|
|
581
|
+
probeApplied
|
|
582
|
+
);
|
|
571
583
|
}
|
|
572
584
|
const request = this.connection.request();
|
|
573
585
|
const messages = [];
|
|
@@ -585,12 +597,16 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
585
597
|
);
|
|
586
598
|
_SQLServerConnector.bindParameters(request, parameters);
|
|
587
599
|
const result = await request.query(processedSQL);
|
|
600
|
+
const resultSets = _SQLServerConnector.buildResultSets(
|
|
601
|
+
result.recordsets,
|
|
602
|
+
result.rowsAffected,
|
|
603
|
+
isSingleStatement ? sqlQuery : void 0
|
|
604
|
+
);
|
|
605
|
+
if (resultSets.length > 0) {
|
|
606
|
+
SQLRowLimiter.flagTruncation(resultSets[0], options.maxRows, probeApplied);
|
|
607
|
+
}
|
|
588
608
|
return {
|
|
589
|
-
resultSets
|
|
590
|
-
result.recordsets,
|
|
591
|
-
result.rowsAffected,
|
|
592
|
-
isSingleStatement ? processedSQL : void 0
|
|
593
|
-
),
|
|
609
|
+
resultSets,
|
|
594
610
|
...messages.length > 0 ? { messages } : {}
|
|
595
611
|
};
|
|
596
612
|
} catch (error) {
|
|
@@ -718,7 +734,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
718
734
|
* Dangerous constructs are rejected before the transaction opens; see
|
|
719
735
|
* assertNoReadOnlyEscapes.
|
|
720
736
|
*/
|
|
721
|
-
async executeReadOnly(processedSQL, parameters,
|
|
737
|
+
async executeReadOnly(processedSQL, parameters, sourceSql, maxRows, probeApplied) {
|
|
722
738
|
this.assertNoReadOnlyEscapes(processedSQL, { transactionControl: true });
|
|
723
739
|
const transaction = new sql.Transaction(this.connection);
|
|
724
740
|
await transaction.begin();
|
|
@@ -754,12 +770,16 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
754
770
|
}
|
|
755
771
|
}
|
|
756
772
|
}
|
|
773
|
+
const resultSets = _SQLServerConnector.buildResultSets(
|
|
774
|
+
result.recordsets,
|
|
775
|
+
result.rowsAffected,
|
|
776
|
+
sourceSql
|
|
777
|
+
);
|
|
778
|
+
if (resultSets.length > 0) {
|
|
779
|
+
SQLRowLimiter.flagTruncation(resultSets[0], maxRows, probeApplied);
|
|
780
|
+
}
|
|
757
781
|
return {
|
|
758
|
-
resultSets
|
|
759
|
-
result.recordsets,
|
|
760
|
-
result.rowsAffected,
|
|
761
|
-
isSingleStatement ? processedSQL : void 0
|
|
762
|
-
),
|
|
782
|
+
resultSets,
|
|
763
783
|
...messages.length > 0 ? { messages } : {}
|
|
764
784
|
};
|
|
765
785
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
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": {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"zod": "^4.2.0"
|
|
56
56
|
},
|
|
57
57
|
"optionalDependencies": {
|
|
58
|
+
"@aws-sdk/credential-providers": "^3.1001.0",
|
|
58
59
|
"@aws-sdk/rds-signer": "^3.1001.0",
|
|
59
60
|
"@azure/identity": "^4.8.0",
|
|
60
61
|
"mariadb": "^3.4.0",
|