@bytebase/dbhub 0.11.2 → 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.
Files changed (3) hide show
  1. package/README.md +30 -2
  2. package/dist/index.js +181 -19
  3. 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
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=dbhub&config=eyJjb21tYW5kIjoibnB4IEBieXRlYmFzZS9kYmh1YiIsImVudiI6eyJUUkFOU1BPUlQiOiJzdGRpbyIsIkRTTiI6InBvc3RncmVzOi8vdXNlcjpwYXNzd29yZEBsb2NhbGhvc3Q6NTQzMi9kYm5hbWU%2Fc3NsbW9kZT1kaXNhYmxlIiwiUkVBRE9OTFkiOiJ0cnVlIn19)
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
  ![cursor](https://raw.githubusercontent.com/bytebase/dbhub/main/resources/images/cursor.webp)
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
- return await client.query(statements[0]);
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 (const statement of statements) {
511
- const result = await client.query(statement);
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
- const result = await this.connection.request().query(sql2);
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(statements[0]).all();
1188
+ const rows = this.db.prepare(processedStatement).all();
1088
1189
  return { rows };
1089
1190
  } else {
1090
- this.db.prepare(statements[0]).run();
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 (const statement of readStatements) {
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
- const results = await this.pool.query(sql2);
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]) && firstResult[0].length === 2) {
1590
+ if (Array.isArray(firstResult) && firstResult.length > 0 && Array.isArray(firstResult[0])) {
1478
1591
  let allRows = [];
1479
- for (const [rows, _fields] of firstResult) {
1480
- if (Array.isArray(rows)) {
1481
- allRows.push(...rows);
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
- const results = await this.pool.query(sql2);
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 = [];
@@ -2219,6 +2343,17 @@ function resolveTransport() {
2219
2343
  }
2220
2344
  return { type: "stdio", source: "default" };
2221
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
+ }
2222
2357
  function resolvePort() {
2223
2358
  const args = parseCommandLineArgs();
2224
2359
  if (args.port) {
@@ -2330,9 +2465,15 @@ var ConnectorManager = class {
2330
2465
  this.connected = false;
2331
2466
  this.sshTunnel = null;
2332
2467
  this.originalDSN = null;
2468
+ this.maxRows = null;
2333
2469
  if (!managerInstance) {
2334
2470
  managerInstance = this;
2335
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
+ }
2336
2477
  }
2337
2478
  /**
2338
2479
  * Initialize and connect to the database using a DSN
@@ -2428,6 +2569,26 @@ var ConnectorManager = class {
2428
2569
  }
2429
2570
  return managerInstance.getConnector();
2430
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
+ }
2431
2592
  /**
2432
2593
  * Get default port for a database based on DSN protocol
2433
2594
  */
@@ -2870,6 +3031,7 @@ function areAllStatementsReadOnly(sql2, connectorType) {
2870
3031
  }
2871
3032
  async function executeSqlToolHandler({ sql: sql2 }, _extra) {
2872
3033
  const connector = ConnectorManager.getCurrentConnector();
3034
+ const executeOptions = ConnectorManager.getCurrentExecuteOptions();
2873
3035
  try {
2874
3036
  if (isReadOnlyMode() && !areAllStatementsReadOnly(sql2, connector.id)) {
2875
3037
  return createToolErrorResponse(
@@ -2877,7 +3039,7 @@ async function executeSqlToolHandler({ sql: sql2 }, _extra) {
2877
3039
  "READONLY_VIOLATION"
2878
3040
  );
2879
3041
  }
2880
- const result = await connector.executeSQL(sql2);
3042
+ const result = await connector.executeSQL(sql2, executeOptions);
2881
3043
  const responseData = {
2882
3044
  rows: result.rows,
2883
3045
  count: result.rows.length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "Universal Database MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",