@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.
- package/README.md +188 -23
- package/dist/index.js +261 -21
- 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
|
-

|
|
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
|
-
|
|
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
|

|
|
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
|
|
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,
|
|
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
|
|
351
|
-
| -------------- | -------------------- |
|
|
352
|
-
| dsn | `DSN` | Database connection string
|
|
353
|
-
|
|
|
354
|
-
|
|
|
355
|
-
|
|
|
356
|
-
|
|
|
357
|
-
|
|
|
358
|
-
|
|
|
359
|
-
|
|
|
360
|
-
|
|
|
361
|
-
|
|
|
362
|
-
|
|
|
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
|
+

|
|
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
|
-
|
|
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) => ({
|
|
@@ -949,10 +1046,19 @@ var SQLiteConnector = class {
|
|
|
949
1046
|
async disconnect() {
|
|
950
1047
|
if (this.db) {
|
|
951
1048
|
try {
|
|
952
|
-
this.db.
|
|
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
|
-
|
|
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(
|
|
1197
|
+
const rows = this.db.prepare(processedStatement).all();
|
|
1088
1198
|
return { rows };
|
|
1089
1199
|
} else {
|
|
1090
|
-
this.db.prepare(
|
|
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 (
|
|
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
|
-
|
|
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])
|
|
1599
|
+
if (Array.isArray(firstResult) && firstResult.length > 0 && Array.isArray(firstResult[0])) {
|
|
1478
1600
|
let allRows = [];
|
|
1479
|
-
for (const
|
|
1480
|
-
if (Array.isArray(
|
|
1481
|
-
allRows.push(...
|
|
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
|
-
|
|
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
|