@bytebase/dbhub 0.11.2 → 0.11.4

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 +188 -23
  2. package/dist/index.js +261 -21
  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
@@ -35,12 +43,6 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
35
43
  MCP Clients MCP Server Databases
36
44
  ```
37
45
 
38
- ## Demo HTTP Endpoint
39
-
40
- https://demo.dbhub.ai/message connects a [sample employee database](https://github.com/bytebase/employee-sample-database). You can point Cursor or MCP Inspector to it to see it in action.
41
-
42
- ![mcp-inspector](https://raw.githubusercontent.com/bytebase/dbhub/main/resources/images/mcp-inspector.webp)
43
-
44
46
  ## Supported Matrix
45
47
 
46
48
  ### Database Resources
@@ -93,6 +95,29 @@ docker run --rm --init \
93
95
  --demo
94
96
  ```
95
97
 
98
+ **Docker Compose Setup:**
99
+
100
+ If you're using Docker Compose for development, add DBHub to your `docker-compose.yml`:
101
+
102
+ ```yaml
103
+ dbhub:
104
+ image: bytebase/dbhub:latest
105
+ container_name: dbhub
106
+ ports:
107
+ - "8080:8080"
108
+ environment:
109
+ - DBHUB_LOG_LEVEL=info
110
+ command:
111
+ - --transport
112
+ - http
113
+ - --port
114
+ - "8080"
115
+ - --dsn
116
+ - "postgres://user:password@database:5432/dbname"
117
+ depends_on:
118
+ - database
119
+ ```
120
+
96
121
  ### NPM
97
122
 
98
123
  ```bash
@@ -159,13 +184,88 @@ Check https://docs.anthropic.com/en/docs/claude-code/mcp
159
184
 
160
185
  ### Cursor
161
186
 
162
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=dbhub&config=eyJjb21tYW5kIjoibnB4IEBieXRlYmFzZS9kYmh1YiIsImVudiI6eyJUUkFOU1BPUlQiOiJzdGRpbyIsIkRTTiI6InBvc3RncmVzOi8vdXNlcjpwYXNzd29yZEBsb2NhbGhvc3Q6NTQzMi9kYm5hbWU%2Fc3NsbW9kZT1kaXNhYmxlIiwiUkVBRE9OTFkiOiJ0cnVlIn19)
187
+ <p>
188
+ Add to Cursor by copying the below link to browser
189
+
190
+ ```text
191
+ cursor://anysphere.cursor-deeplink/mcp/install?name=dbhub&config=eyJjb21tYW5kIjoibnB4IEBieXRlYmFzZS9kYmh1YiIsImVudiI6eyJUUkFOU1BPUlQiOiJzdGRpbyIsIkRTTiI6InBvc3RncmVzOi8vdXNlcjpwYXNzd29yZEBsb2NhbGhvc3Q6NTQzMi9kYm5hbWU%2Fc3NsbW9kZT1kaXNhYmxlIiwiUkVBRE9OTFkiOiJ0cnVlIn19
192
+ ```
193
+
194
+ </p>
163
195
 
164
196
  ![cursor](https://raw.githubusercontent.com/bytebase/dbhub/main/resources/images/cursor.webp)
165
197
 
166
198
  - Cursor supports both `stdio` and `http`.
167
199
  - Follow [Cursor MCP guide](https://docs.cursor.com/context/model-context-protocol) and make sure to use [Agent](https://docs.cursor.com/chat/agent) mode.
168
200
 
201
+ ### VSCode + Copilot
202
+
203
+ Check https://code.visualstudio.com/docs/copilot/customization/mcp-servers
204
+
205
+ VSCode with GitHub Copilot can connect to DBHub via both `stdio` and `http` transports. This enables AI agents to interact with your development database through a secure interface.
206
+
207
+ - VSCode supports both `stdio` and `http` transports
208
+ - Configure MCP server in `.vscode/mcp.json`:
209
+
210
+ **Stdio Transport:**
211
+
212
+ ```json
213
+ {
214
+ "servers": {
215
+ "dbhub": {
216
+ "command": "npx",
217
+ "args": [
218
+ "-y",
219
+ "@bytebase/dbhub",
220
+ "--transport",
221
+ "stdio",
222
+ "--dsn",
223
+ "postgres://user:password@localhost:5432/dbname"
224
+ ]
225
+ }
226
+ },
227
+ "inputs": []
228
+ }
229
+ ```
230
+
231
+ **HTTP Transport:**
232
+
233
+ ```json
234
+ {
235
+ "servers": {
236
+ "dbhub": {
237
+ "url": "http://localhost:8080/message",
238
+ "type": "http"
239
+ }
240
+ },
241
+ "inputs": []
242
+ }
243
+ ```
244
+
245
+ **Copilot Instructions:**
246
+
247
+ You can provide Copilot with context by creating `.github/copilot-instructions.md`:
248
+
249
+ ```markdown
250
+ ## Database Access
251
+
252
+ This project provides an MCP server (DBHub) for secure SQL access to the development database.
253
+
254
+ AI agents can execute SQL queries. In read-only mode (recommended for production):
255
+
256
+ - `SELECT * FROM users LIMIT 5;`
257
+ - `SHOW TABLES;`
258
+ - `DESCRIBE table_name;`
259
+
260
+ In read-write mode (development/testing):
261
+
262
+ - `INSERT INTO users (name, email) VALUES ('John', 'john@example.com');`
263
+ - `UPDATE users SET status = 'active' WHERE id = 1;`
264
+ - `CREATE TABLE test_table (id INT PRIMARY KEY);`
265
+
266
+ Use `--readonly` flag to restrict to read-only operations for safety.
267
+ ```
268
+
169
269
  ## Usage
170
270
 
171
271
  ### Read-only Mode
@@ -181,6 +281,18 @@ In read-only mode, only [readonly SQL operations](https://github.com/bytebase/db
181
281
 
182
282
  This provides an additional layer of security when connecting to production databases.
183
283
 
284
+ ### Row Limiting
285
+
286
+ 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:
287
+
288
+ ```bash
289
+ # Limit SELECT queries to return at most 1000 rows
290
+ npx @bytebase/dbhub --dsn "postgres://user:password@localhost:5432/dbname" --max-rows 1000
291
+ ```
292
+
293
+ - Row limiting is only applied to SELECT statements, not INSERT/UPDATE/DELETE
294
+ - If your query already has a `LIMIT` or `TOP` clause, DBHub uses the smaller value
295
+
184
296
  ### SSL Connections
185
297
 
186
298
  You can specify the SSL mode using the `sslmode` parameter in your DSN string:
@@ -287,9 +399,14 @@ npx @bytebase/dbhub --demo
287
399
  ```
288
400
 
289
401
  > [!WARNING]
290
- > If your user/password contains special characters, you need to escape them first. (e.g. `pass#word` should be escaped as `pass%23word`)
402
+ > If your user/password contains special characters, you have two options:
403
+ >
404
+ > 1. Escape them in the DSN (e.g. `pass#word` should be escaped as `pass%23word`)
405
+ > 2. Use the individual database parameters method below (recommended)
291
406
 
292
- For real databases, a Database Source Name (DSN) is required. You can provide this in several ways:
407
+ For real databases, you can configure the database connection in two ways:
408
+
409
+ #### Method 1: Database Source Name (DSN)
293
410
 
294
411
  - **Command line argument** (highest priority):
295
412
 
@@ -311,6 +428,45 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
311
428
  DSN=postgres://user:password@localhost:5432/dbname?sslmode=disable
312
429
  ```
313
430
 
431
+ #### Method 2: Individual Database Parameters
432
+
433
+ If your password contains special characters that would break URL parsing, use individual environment variables instead:
434
+
435
+ - **Environment variables**:
436
+
437
+ ```bash
438
+ export DB_TYPE=postgres
439
+ export DB_HOST=localhost
440
+ export DB_PORT=5432
441
+ export DB_USER=myuser
442
+ export DB_PASSWORD='my@complex:password/with#special&chars'
443
+ export DB_NAME=mydatabase
444
+ npx @bytebase/dbhub
445
+ ```
446
+
447
+ - **Environment file**:
448
+ ```
449
+ DB_TYPE=postgres
450
+ DB_HOST=localhost
451
+ DB_PORT=5432
452
+ DB_USER=myuser
453
+ DB_PASSWORD=my@complex:password/with#special&chars
454
+ DB_NAME=mydatabase
455
+ ```
456
+
457
+ **Supported DB_TYPE values**: `postgres`, `mysql`, `mariadb`, `sqlserver`, `sqlite`
458
+
459
+ **Default ports** (when DB_PORT is omitted):
460
+
461
+ - PostgreSQL: `5432`
462
+ - MySQL/MariaDB: `3306`
463
+ - SQL Server: `1433`
464
+
465
+ **For SQLite**: Only `DB_TYPE=sqlite` and `DB_NAME=/path/to/database.db` are required.
466
+
467
+ > [!TIP]
468
+ > Use the individual parameter method when your password contains special characters like `@`, `:`, `/`, `#`, `&`, `=` that would break DSN parsing.
469
+
314
470
  > [!WARNING]
315
471
  > When running in Docker, use `host.docker.internal` instead of `localhost` to connect to databases running on your host machine. For example: `mysql://user:password@host.docker.internal:3306/dbname`
316
472
 
@@ -347,19 +503,26 @@ Extra query parameters:
347
503
 
348
504
  ### Command line options
349
505
 
350
- | Option | Environment Variable | Description | Default |
351
- | -------------- | -------------------- | ---------------------------------------------------------------- | ---------------------------- |
352
- | dsn | `DSN` | Database connection string | Required if not in demo mode |
353
- | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
354
- | port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
355
- | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
356
- | demo | N/A | Run in demo mode with sample employee database | `false` |
357
- | ssh-host | `SSH_HOST` | SSH server hostname for tunnel connection | N/A |
358
- | ssh-port | `SSH_PORT` | SSH server port | `22` |
359
- | ssh-user | `SSH_USER` | SSH username | N/A |
360
- | ssh-password | `SSH_PASSWORD` | SSH password (for password authentication) | N/A |
361
- | ssh-key | `SSH_KEY` | Path to SSH private key file | N/A |
362
- | ssh-passphrase | `SSH_PASSPHRASE` | Passphrase for SSH private key | N/A |
506
+ | Option | Environment Variable | Description | Default |
507
+ | -------------- | -------------------- | --------------------------------------------------------------------- | ---------------------------- |
508
+ | dsn | `DSN` | Database connection string | Required if not in demo mode |
509
+ | N/A | `DB_TYPE` | Database type: `postgres`, `mysql`, `mariadb`, `sqlserver`, `sqlite` | N/A |
510
+ | N/A | `DB_HOST` | Database server hostname (not needed for SQLite) | N/A |
511
+ | N/A | `DB_PORT` | Database server port (uses default if omitted, not needed for SQLite) | N/A |
512
+ | N/A | `DB_USER` | Database username (not needed for SQLite) | N/A |
513
+ | N/A | `DB_PASSWORD` | Database password (not needed for SQLite) | N/A |
514
+ | N/A | `DB_NAME` | Database name or SQLite file path | N/A |
515
+ | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
516
+ | port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
517
+ | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
518
+ | max-rows | N/A | Limit the number of rows returned from SELECT queries | No limit |
519
+ | demo | N/A | Run in demo mode with sample employee database | `false` |
520
+ | ssh-host | `SSH_HOST` | SSH server hostname for tunnel connection | N/A |
521
+ | ssh-port | `SSH_PORT` | SSH server port | `22` |
522
+ | ssh-user | `SSH_USER` | SSH username | N/A |
523
+ | ssh-password | `SSH_PASSWORD` | SSH password (for password authentication) | N/A |
524
+ | ssh-key | `SSH_KEY` | Path to SSH private key file | N/A |
525
+ | ssh-passphrase | `SSH_PASSPHRASE` | Passphrase for SSH private key | N/A |
363
526
 
364
527
  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.
365
528
 
@@ -483,6 +646,8 @@ The project includes pre-commit hooks to run tests automatically before each com
483
646
 
484
647
  ### Debug with [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
485
648
 
649
+ ![mcp-inspector](https://raw.githubusercontent.com/bytebase/dbhub/main/resources/images/mcp-inspector.webp)
650
+
486
651
  #### stdio
487
652
 
488
653
  ```bash
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) => ({
@@ -949,10 +1046,19 @@ var SQLiteConnector = class {
949
1046
  async disconnect() {
950
1047
  if (this.db) {
951
1048
  try {
952
- this.db.close();
1049
+ if (!this.db.inTransaction) {
1050
+ this.db.close();
1051
+ } else {
1052
+ try {
1053
+ this.db.exec("ROLLBACK");
1054
+ } catch (rollbackError) {
1055
+ }
1056
+ this.db.close();
1057
+ }
953
1058
  this.db = null;
954
1059
  } catch (error) {
955
- throw error;
1060
+ console.error("Error during SQLite disconnect:", error);
1061
+ this.db = null;
956
1062
  }
957
1063
  }
958
1064
  return Promise.resolve();
@@ -1074,20 +1180,24 @@ var SQLiteConnector = class {
1074
1180
  "SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
1075
1181
  );
1076
1182
  }
1077
- async executeSQL(sql2) {
1183
+ async executeSQL(sql2, options) {
1078
1184
  if (!this.db) {
1079
1185
  throw new Error("Not connected to SQLite database");
1080
1186
  }
1081
1187
  try {
1082
1188
  const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
1083
1189
  if (statements.length === 1) {
1190
+ let processedStatement = statements[0];
1084
1191
  const trimmedStatement = statements[0].toLowerCase().trim();
1085
1192
  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"));
1193
+ if (options.maxRows) {
1194
+ processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows);
1195
+ }
1086
1196
  if (isReadStatement) {
1087
- const rows = this.db.prepare(statements[0]).all();
1197
+ const rows = this.db.prepare(processedStatement).all();
1088
1198
  return { rows };
1089
1199
  } else {
1090
- this.db.prepare(statements[0]).run();
1200
+ this.db.prepare(processedStatement).run();
1091
1201
  return { rows: [] };
1092
1202
  }
1093
1203
  } else {
@@ -1105,7 +1215,8 @@ var SQLiteConnector = class {
1105
1215
  this.db.exec(writeStatements.join("; "));
1106
1216
  }
1107
1217
  let allRows = [];
1108
- for (const statement of readStatements) {
1218
+ for (let statement of readStatements) {
1219
+ statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
1109
1220
  const result = this.db.prepare(statement).all();
1110
1221
  allRows.push(...result);
1111
1222
  }
@@ -1467,18 +1578,29 @@ var MySQLConnector = class {
1467
1578
  const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
1468
1579
  return rows[0].DB;
1469
1580
  }
1470
- async executeSQL(sql2) {
1581
+ async executeSQL(sql2, options) {
1471
1582
  if (!this.pool) {
1472
1583
  throw new Error("Not connected to database");
1473
1584
  }
1474
1585
  try {
1475
- const results = await this.pool.query(sql2);
1586
+ let processedSQL = sql2;
1587
+ if (options.maxRows) {
1588
+ const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
1589
+ const processedStatements = statements.map(
1590
+ (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
1591
+ );
1592
+ processedSQL = processedStatements.join("; ");
1593
+ if (sql2.trim().endsWith(";")) {
1594
+ processedSQL += ";";
1595
+ }
1596
+ }
1597
+ const results = await this.pool.query(processedSQL);
1476
1598
  const [firstResult] = results;
1477
- if (Array.isArray(firstResult) && firstResult.length > 0 && Array.isArray(firstResult[0]) && firstResult[0].length === 2) {
1599
+ if (Array.isArray(firstResult) && firstResult.length > 0 && Array.isArray(firstResult[0])) {
1478
1600
  let allRows = [];
1479
- for (const [rows, _fields] of firstResult) {
1480
- if (Array.isArray(rows)) {
1481
- allRows.push(...rows);
1601
+ for (const result of firstResult) {
1602
+ if (Array.isArray(result)) {
1603
+ allRows.push(...result);
1482
1604
  }
1483
1605
  }
1484
1606
  return { rows: allRows };
@@ -1845,12 +1967,23 @@ var MariaDBConnector = class {
1845
1967
  const rows = await this.pool.query("SELECT DATABASE() AS DB");
1846
1968
  return rows[0].DB;
1847
1969
  }
1848
- async executeSQL(sql2) {
1970
+ async executeSQL(sql2, options) {
1849
1971
  if (!this.pool) {
1850
1972
  throw new Error("Not connected to database");
1851
1973
  }
1852
1974
  try {
1853
- const results = await this.pool.query(sql2);
1975
+ let processedSQL = sql2;
1976
+ if (options.maxRows) {
1977
+ const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
1978
+ const processedStatements = statements.map(
1979
+ (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
1980
+ );
1981
+ processedSQL = processedStatements.join("; ");
1982
+ if (sql2.trim().endsWith(";")) {
1983
+ processedSQL += ";";
1984
+ }
1985
+ }
1986
+ const results = await this.pool.query(processedSQL);
1854
1987
  if (Array.isArray(results)) {
1855
1988
  if (results.length > 0 && Array.isArray(results[0]) && results[0].length > 0) {
1856
1989
  let allRows = [];
@@ -2186,6 +2319,62 @@ function isReadOnlyMode() {
2186
2319
  }
2187
2320
  return false;
2188
2321
  }
2322
+ function buildDSNFromEnvParams() {
2323
+ const dbType = process.env.DB_TYPE;
2324
+ const dbHost = process.env.DB_HOST;
2325
+ const dbUser = process.env.DB_USER;
2326
+ const dbPassword = process.env.DB_PASSWORD;
2327
+ const dbName = process.env.DB_NAME;
2328
+ const dbPort = process.env.DB_PORT;
2329
+ if (dbType?.toLowerCase() === "sqlite") {
2330
+ if (!dbName) {
2331
+ return null;
2332
+ }
2333
+ } else {
2334
+ if (!dbType || !dbHost || !dbUser || !dbPassword || !dbName) {
2335
+ return null;
2336
+ }
2337
+ }
2338
+ const supportedTypes = ["postgres", "postgresql", "mysql", "mariadb", "sqlserver", "sqlite"];
2339
+ if (!supportedTypes.includes(dbType.toLowerCase())) {
2340
+ throw new Error(`Unsupported DB_TYPE: ${dbType}. Supported types: ${supportedTypes.join(", ")}`);
2341
+ }
2342
+ let port = dbPort;
2343
+ if (!port) {
2344
+ switch (dbType.toLowerCase()) {
2345
+ case "postgres":
2346
+ case "postgresql":
2347
+ port = "5432";
2348
+ break;
2349
+ case "mysql":
2350
+ case "mariadb":
2351
+ port = "3306";
2352
+ break;
2353
+ case "sqlserver":
2354
+ port = "1433";
2355
+ break;
2356
+ case "sqlite":
2357
+ return {
2358
+ dsn: `sqlite:///${dbName}`,
2359
+ source: "individual environment variables"
2360
+ };
2361
+ default:
2362
+ throw new Error(`Unknown database type for port determination: ${dbType}`);
2363
+ }
2364
+ }
2365
+ const user = dbUser;
2366
+ const password = dbPassword;
2367
+ const dbNameStr = dbName;
2368
+ const encodedUser = encodeURIComponent(user);
2369
+ const encodedPassword = encodeURIComponent(password);
2370
+ const encodedDbName = encodeURIComponent(dbNameStr);
2371
+ const protocol = dbType.toLowerCase() === "postgresql" ? "postgres" : dbType.toLowerCase();
2372
+ const dsn = `${protocol}://${encodedUser}:${encodedPassword}@${dbHost}:${port}/${encodedDbName}`;
2373
+ return {
2374
+ dsn,
2375
+ source: "individual environment variables"
2376
+ };
2377
+ }
2189
2378
  function resolveDSN() {
2190
2379
  const args = parseCommandLineArgs();
2191
2380
  if (isDemoMode()) {
@@ -2201,10 +2390,23 @@ function resolveDSN() {
2201
2390
  if (process.env.DSN) {
2202
2391
  return { dsn: process.env.DSN, source: "environment variable" };
2203
2392
  }
2393
+ const envParamsResult = buildDSNFromEnvParams();
2394
+ if (envParamsResult) {
2395
+ return envParamsResult;
2396
+ }
2204
2397
  const loadedEnvFile = loadEnvFiles();
2205
2398
  if (loadedEnvFile && process.env.DSN) {
2206
2399
  return { dsn: process.env.DSN, source: `${loadedEnvFile} file` };
2207
2400
  }
2401
+ if (loadedEnvFile) {
2402
+ const envFileParamsResult = buildDSNFromEnvParams();
2403
+ if (envFileParamsResult) {
2404
+ return {
2405
+ dsn: envFileParamsResult.dsn,
2406
+ source: `${loadedEnvFile} file (individual parameters)`
2407
+ };
2408
+ }
2409
+ }
2208
2410
  return null;
2209
2411
  }
2210
2412
  function resolveTransport() {
@@ -2219,6 +2421,17 @@ function resolveTransport() {
2219
2421
  }
2220
2422
  return { type: "stdio", source: "default" };
2221
2423
  }
2424
+ function resolveMaxRows() {
2425
+ const args = parseCommandLineArgs();
2426
+ if (args["max-rows"]) {
2427
+ const maxRows = parseInt(args["max-rows"], 10);
2428
+ if (isNaN(maxRows) || maxRows <= 0) {
2429
+ throw new Error(`Invalid --max-rows value: ${args["max-rows"]}. Must be a positive integer.`);
2430
+ }
2431
+ return { maxRows, source: "command line argument" };
2432
+ }
2433
+ return null;
2434
+ }
2222
2435
  function resolvePort() {
2223
2436
  const args = parseCommandLineArgs();
2224
2437
  if (args.port) {
@@ -2330,9 +2543,15 @@ var ConnectorManager = class {
2330
2543
  this.connected = false;
2331
2544
  this.sshTunnel = null;
2332
2545
  this.originalDSN = null;
2546
+ this.maxRows = null;
2333
2547
  if (!managerInstance) {
2334
2548
  managerInstance = this;
2335
2549
  }
2550
+ const maxRowsData = resolveMaxRows();
2551
+ if (maxRowsData) {
2552
+ this.maxRows = maxRowsData.maxRows;
2553
+ console.error(`Max rows limit: ${this.maxRows} (from ${maxRowsData.source})`);
2554
+ }
2336
2555
  }
2337
2556
  /**
2338
2557
  * Initialize and connect to the database using a DSN
@@ -2428,6 +2647,26 @@ var ConnectorManager = class {
2428
2647
  }
2429
2648
  return managerInstance.getConnector();
2430
2649
  }
2650
+ /**
2651
+ * Get execute options for SQL execution
2652
+ */
2653
+ getExecuteOptions() {
2654
+ const options = {};
2655
+ if (this.maxRows !== null) {
2656
+ options.maxRows = this.maxRows;
2657
+ }
2658
+ return options;
2659
+ }
2660
+ /**
2661
+ * Get the current execute options
2662
+ * This is used by tool handlers
2663
+ */
2664
+ static getCurrentExecuteOptions() {
2665
+ if (!managerInstance) {
2666
+ throw new Error("ConnectorManager not initialized");
2667
+ }
2668
+ return managerInstance.getExecuteOptions();
2669
+ }
2431
2670
  /**
2432
2671
  * Get default port for a database based on DSN protocol
2433
2672
  */
@@ -2870,6 +3109,7 @@ function areAllStatementsReadOnly(sql2, connectorType) {
2870
3109
  }
2871
3110
  async function executeSqlToolHandler({ sql: sql2 }, _extra) {
2872
3111
  const connector = ConnectorManager.getCurrentConnector();
3112
+ const executeOptions = ConnectorManager.getCurrentExecuteOptions();
2873
3113
  try {
2874
3114
  if (isReadOnlyMode() && !areAllStatementsReadOnly(sql2, connector.id)) {
2875
3115
  return createToolErrorResponse(
@@ -2877,7 +3117,7 @@ async function executeSqlToolHandler({ sql: sql2 }, _extra) {
2877
3117
  "READONLY_VIOLATION"
2878
3118
  );
2879
3119
  }
2880
- const result = await connector.executeSQL(sql2);
3120
+ const result = await connector.executeSQL(sql2, executeOptions);
2881
3121
  const responseData = {
2882
3122
  rows: result.rows,
2883
3123
  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.4",
4
4
  "description": "Universal Database MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",