@bytebase/dbhub 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/{chunk-WGDSRFBW.js → chunk-TPHNNFR5.js} +15 -34
- package/dist/index.js +154 -82
- package/dist/public/assets/index-BJ-1UrcV.css +1 -0
- package/dist/public/assets/index-DBYlgGks.js +147 -0
- package/dist/public/index.html +2 -2
- package/dist/{registry-FVGT25UH.js → registry-XXEL5IXH.js} +1 -1
- package/package.json +1 -1
- package/dist/public/assets/index-gVrYRID4.css +0 -1
- package/dist/public/assets/index-hd88eD9m.js +0 -51
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
MCP Clients MCP Server Databases
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
DBHub is a
|
|
33
|
+
DBHub is a zero-dependency, minimal database MCP server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
|
|
34
34
|
|
|
35
35
|
- **Token Efficient**: Just two general MCP tools (execute_sql, search_objects) to minimize context window usage, plus support for custom tools
|
|
36
36
|
- **Multi-Database**: Single interface for PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite
|
|
@@ -50,6 +50,12 @@ DBHub implements MCP tools for database operations:
|
|
|
50
50
|
- **[search_objects](https://dbhub.ai/tools/search-objects)**: Search and explore database schemas, tables, columns, indexes, and procedures with progressive disclosure
|
|
51
51
|
- **[Custom Tools](https://dbhub.ai/tools/custom-tools)**: Define reusable, parameterized SQL operations in your `dbhub.toml` configuration file
|
|
52
52
|
|
|
53
|
+
## Workbench
|
|
54
|
+
|
|
55
|
+
DBHub includes a [built-in web interface](https://dbhub.ai/workbench/overview) for interacting with your database tools. It provides a visual way to execute queries, run custom tools, and view request traces without requiring an MCP client.
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
53
59
|
## Installation
|
|
54
60
|
|
|
55
61
|
See the full [Installation Guide](https://dbhub.ai/installation) for detailed instructions.
|
|
@@ -80,13 +86,13 @@ npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:p
|
|
|
80
86
|
npx @bytebase/dbhub@latest --transport http --port 8080 --demo
|
|
81
87
|
```
|
|
82
88
|
|
|
83
|
-
See [
|
|
89
|
+
See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
|
|
84
90
|
|
|
85
91
|
### Multi-Database Setup
|
|
86
92
|
|
|
87
93
|
Connect to multiple databases simultaneously using TOML configuration files. Perfect for managing production, staging, and development databases from a single DBHub instance.
|
|
88
94
|
|
|
89
|
-
See [Multi-Database Configuration](https://dbhub.ai/config/
|
|
95
|
+
See [Multi-Database Configuration](https://dbhub.ai/config/toml) for complete setup instructions.
|
|
90
96
|
|
|
91
97
|
## Development
|
|
92
98
|
|
|
@@ -1237,11 +1237,6 @@ function validateToolsConfig(tools, sources, configPath) {
|
|
|
1237
1237
|
`Configuration file ${configPath}: custom tool '${tool.name}' must have 'description' and 'statement' fields`
|
|
1238
1238
|
);
|
|
1239
1239
|
}
|
|
1240
|
-
if (tool.readonly !== void 0 || tool.max_rows !== void 0) {
|
|
1241
|
-
throw new Error(
|
|
1242
|
-
`Configuration file ${configPath}: custom tool '${tool.name}' cannot have readonly or max_rows fields (these are only valid for ${BUILTIN_TOOL_EXECUTE_SQL} tool)`
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
1240
|
}
|
|
1246
1241
|
if (tool.max_rows !== void 0) {
|
|
1247
1242
|
if (typeof tool.max_rows !== "number" || tool.max_rows <= 0) {
|
|
@@ -1250,6 +1245,11 @@ function validateToolsConfig(tools, sources, configPath) {
|
|
|
1250
1245
|
);
|
|
1251
1246
|
}
|
|
1252
1247
|
}
|
|
1248
|
+
if (tool.readonly !== void 0 && typeof tool.readonly !== "boolean") {
|
|
1249
|
+
throw new Error(
|
|
1250
|
+
`Configuration file ${configPath}: tool '${tool.name}' has invalid readonly. Must be a boolean (true or false).`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
1253
|
}
|
|
1254
1254
|
}
|
|
1255
1255
|
function validateSourceConfig(source, configPath) {
|
|
@@ -1339,6 +1339,16 @@ function validateSourceConfig(source, configPath) {
|
|
|
1339
1339
|
);
|
|
1340
1340
|
}
|
|
1341
1341
|
}
|
|
1342
|
+
if (source.readonly !== void 0) {
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
`Configuration file ${configPath}: source '${source.id}' has 'readonly' field, but readonly must be configured per-tool, not per-source. Move 'readonly' to [[tools]] configuration instead.`
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
if (source.max_rows !== void 0) {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`Configuration file ${configPath}: source '${source.id}' has 'max_rows' field, but max_rows must be configured per-tool, not per-source. Move 'max_rows' to [[tools]] configuration instead.`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1342
1352
|
}
|
|
1343
1353
|
function processSourceConfigs(sources, configPath) {
|
|
1344
1354
|
return sources.map((source) => {
|
|
@@ -1446,7 +1456,6 @@ var ConnectorManager = class {
|
|
|
1446
1456
|
// Maps for multi-source support
|
|
1447
1457
|
this.connectors = /* @__PURE__ */ new Map();
|
|
1448
1458
|
this.sshTunnels = /* @__PURE__ */ new Map();
|
|
1449
|
-
this.executeOptions = /* @__PURE__ */ new Map();
|
|
1450
1459
|
this.sourceConfigs = /* @__PURE__ */ new Map();
|
|
1451
1460
|
// Store original source configs
|
|
1452
1461
|
this.sourceIds = [];
|
|
@@ -1533,14 +1542,6 @@ var ConnectorManager = class {
|
|
|
1533
1542
|
this.connectors.set(sourceId, connector);
|
|
1534
1543
|
this.sourceIds.push(sourceId);
|
|
1535
1544
|
this.sourceConfigs.set(sourceId, source);
|
|
1536
|
-
const options = {};
|
|
1537
|
-
if (source.max_rows !== void 0) {
|
|
1538
|
-
options.maxRows = source.max_rows;
|
|
1539
|
-
}
|
|
1540
|
-
if (source.readonly !== void 0) {
|
|
1541
|
-
options.readonly = source.readonly;
|
|
1542
|
-
}
|
|
1543
|
-
this.executeOptions.set(sourceId, options);
|
|
1544
1545
|
}
|
|
1545
1546
|
/**
|
|
1546
1547
|
* Close all database connections
|
|
@@ -1563,7 +1564,6 @@ var ConnectorManager = class {
|
|
|
1563
1564
|
}
|
|
1564
1565
|
this.connectors.clear();
|
|
1565
1566
|
this.sshTunnels.clear();
|
|
1566
|
-
this.executeOptions.clear();
|
|
1567
1567
|
this.sourceConfigs.clear();
|
|
1568
1568
|
this.sourceIds = [];
|
|
1569
1569
|
}
|
|
@@ -1608,25 +1608,6 @@ var ConnectorManager = class {
|
|
|
1608
1608
|
}
|
|
1609
1609
|
return managerInstance.getConnector(sourceId);
|
|
1610
1610
|
}
|
|
1611
|
-
/**
|
|
1612
|
-
* Get execute options for SQL execution
|
|
1613
|
-
* @param sourceId - Optional source ID. If not provided, returns default options
|
|
1614
|
-
*/
|
|
1615
|
-
getExecuteOptions(sourceId) {
|
|
1616
|
-
const id = sourceId || this.sourceIds[0];
|
|
1617
|
-
return this.executeOptions.get(id) || {};
|
|
1618
|
-
}
|
|
1619
|
-
/**
|
|
1620
|
-
* Get the current execute options
|
|
1621
|
-
* This is used by tool handlers
|
|
1622
|
-
* @param sourceId - Optional source ID. If not provided, returns default options
|
|
1623
|
-
*/
|
|
1624
|
-
static getCurrentExecuteOptions(sourceId) {
|
|
1625
|
-
if (!managerInstance) {
|
|
1626
|
-
throw new Error("ConnectorManager not initialized");
|
|
1627
|
-
}
|
|
1628
|
-
return managerInstance.getExecuteOptions(sourceId);
|
|
1629
|
-
}
|
|
1630
1611
|
/**
|
|
1631
1612
|
* Get all available source IDs
|
|
1632
1613
|
*/
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
resolveSourceConfigs,
|
|
17
17
|
resolveTransport,
|
|
18
18
|
stripCommentsAndStrings
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-TPHNNFR5.js";
|
|
20
20
|
|
|
21
21
|
// src/connectors/postgres/index.ts
|
|
22
22
|
import pg from "pg";
|
|
@@ -477,22 +477,26 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
477
477
|
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
478
478
|
if (statements.length === 1) {
|
|
479
479
|
const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
|
|
480
|
+
let result;
|
|
480
481
|
if (parameters && parameters.length > 0) {
|
|
481
482
|
try {
|
|
482
|
-
|
|
483
|
+
result = await client.query(processedStatement, parameters);
|
|
483
484
|
} catch (error) {
|
|
484
485
|
console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
|
|
485
486
|
console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
|
|
486
487
|
console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
487
488
|
throw error;
|
|
488
489
|
}
|
|
490
|
+
} else {
|
|
491
|
+
result = await client.query(processedStatement);
|
|
489
492
|
}
|
|
490
|
-
return
|
|
493
|
+
return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length };
|
|
491
494
|
} else {
|
|
492
495
|
if (parameters && parameters.length > 0) {
|
|
493
496
|
throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
|
|
494
497
|
}
|
|
495
498
|
let allRows = [];
|
|
499
|
+
let totalRowCount = 0;
|
|
496
500
|
await client.query("BEGIN");
|
|
497
501
|
try {
|
|
498
502
|
for (let statement of statements) {
|
|
@@ -501,13 +505,16 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
501
505
|
if (result.rows && result.rows.length > 0) {
|
|
502
506
|
allRows.push(...result.rows);
|
|
503
507
|
}
|
|
508
|
+
if (result.rowCount) {
|
|
509
|
+
totalRowCount += result.rowCount;
|
|
510
|
+
}
|
|
504
511
|
}
|
|
505
512
|
await client.query("COMMIT");
|
|
506
513
|
} catch (error) {
|
|
507
514
|
await client.query("ROLLBACK");
|
|
508
515
|
throw error;
|
|
509
516
|
}
|
|
510
|
-
return { rows: allRows };
|
|
517
|
+
return { rows: allRows, rowCount: totalRowCount };
|
|
511
518
|
}
|
|
512
519
|
} finally {
|
|
513
520
|
client.release();
|
|
@@ -929,9 +936,6 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
929
936
|
}
|
|
930
937
|
return {
|
|
931
938
|
rows: result.recordset || [],
|
|
932
|
-
fields: result.recordset && result.recordset.length > 0 ? Object.keys(result.recordset[0]).map((key) => ({
|
|
933
|
-
name: key
|
|
934
|
-
})) : [],
|
|
935
939
|
rowCount: result.rowsAffected[0] || 0
|
|
936
940
|
};
|
|
937
941
|
} catch (error) {
|
|
@@ -1215,7 +1219,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1215
1219
|
if (parameters && parameters.length > 0) {
|
|
1216
1220
|
try {
|
|
1217
1221
|
const rows = this.db.prepare(processedStatement).all(...parameters);
|
|
1218
|
-
return { rows };
|
|
1222
|
+
return { rows, rowCount: rows.length };
|
|
1219
1223
|
} catch (error) {
|
|
1220
1224
|
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
1221
1225
|
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
@@ -1224,12 +1228,13 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1224
1228
|
}
|
|
1225
1229
|
} else {
|
|
1226
1230
|
const rows = this.db.prepare(processedStatement).all();
|
|
1227
|
-
return { rows };
|
|
1231
|
+
return { rows, rowCount: rows.length };
|
|
1228
1232
|
}
|
|
1229
1233
|
} else {
|
|
1234
|
+
let result;
|
|
1230
1235
|
if (parameters && parameters.length > 0) {
|
|
1231
1236
|
try {
|
|
1232
|
-
this.db.prepare(processedStatement).run(...parameters);
|
|
1237
|
+
result = this.db.prepare(processedStatement).run(...parameters);
|
|
1233
1238
|
} catch (error) {
|
|
1234
1239
|
console.error(`[SQLite executeSQL] ERROR: ${error.message}`);
|
|
1235
1240
|
console.error(`[SQLite executeSQL] SQL: ${processedStatement}`);
|
|
@@ -1237,9 +1242,9 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1237
1242
|
throw error;
|
|
1238
1243
|
}
|
|
1239
1244
|
} else {
|
|
1240
|
-
this.db.prepare(processedStatement).run();
|
|
1245
|
+
result = this.db.prepare(processedStatement).run();
|
|
1241
1246
|
}
|
|
1242
|
-
return { rows: [] };
|
|
1247
|
+
return { rows: [], rowCount: result.changes };
|
|
1243
1248
|
}
|
|
1244
1249
|
} else {
|
|
1245
1250
|
if (parameters && parameters.length > 0) {
|
|
@@ -1255,8 +1260,10 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1255
1260
|
writeStatements.push(statement);
|
|
1256
1261
|
}
|
|
1257
1262
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1263
|
+
let totalChanges = 0;
|
|
1264
|
+
for (const statement of writeStatements) {
|
|
1265
|
+
const result = this.db.prepare(statement).run();
|
|
1266
|
+
totalChanges += result.changes;
|
|
1260
1267
|
}
|
|
1261
1268
|
let allRows = [];
|
|
1262
1269
|
for (let statement of readStatements) {
|
|
@@ -1264,7 +1271,7 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1264
1271
|
const result = this.db.prepare(statement).all();
|
|
1265
1272
|
allRows.push(...result);
|
|
1266
1273
|
}
|
|
1267
|
-
return { rows: allRows };
|
|
1274
|
+
return { rows: allRows, rowCount: totalChanges + allRows.length };
|
|
1268
1275
|
}
|
|
1269
1276
|
} catch (error) {
|
|
1270
1277
|
throw error;
|
|
@@ -1303,6 +1310,26 @@ function extractRowsFromMultiStatement(results) {
|
|
|
1303
1310
|
}
|
|
1304
1311
|
return allRows;
|
|
1305
1312
|
}
|
|
1313
|
+
function extractAffectedRows(results) {
|
|
1314
|
+
if (isMetadataObject(results)) {
|
|
1315
|
+
return results.affectedRows || 0;
|
|
1316
|
+
}
|
|
1317
|
+
if (!Array.isArray(results)) {
|
|
1318
|
+
return 0;
|
|
1319
|
+
}
|
|
1320
|
+
if (isMultiStatementResult(results)) {
|
|
1321
|
+
let totalAffected = 0;
|
|
1322
|
+
for (const result of results) {
|
|
1323
|
+
if (isMetadataObject(result)) {
|
|
1324
|
+
totalAffected += result.affectedRows || 0;
|
|
1325
|
+
} else if (Array.isArray(result)) {
|
|
1326
|
+
totalAffected += result.length;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return totalAffected;
|
|
1330
|
+
}
|
|
1331
|
+
return results.length;
|
|
1332
|
+
}
|
|
1306
1333
|
function parseQueryResults(results) {
|
|
1307
1334
|
if (!Array.isArray(results)) {
|
|
1308
1335
|
return [];
|
|
@@ -1716,7 +1743,8 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
1716
1743
|
}
|
|
1717
1744
|
const [firstResult] = results;
|
|
1718
1745
|
const rows = parseQueryResults(firstResult);
|
|
1719
|
-
|
|
1746
|
+
const rowCount = extractAffectedRows(firstResult);
|
|
1747
|
+
return { rows, rowCount };
|
|
1720
1748
|
} catch (error) {
|
|
1721
1749
|
console.error("Error executing query:", error);
|
|
1722
1750
|
throw error;
|
|
@@ -2127,7 +2155,8 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
2127
2155
|
results = await conn.query(processedSQL);
|
|
2128
2156
|
}
|
|
2129
2157
|
const rows = parseQueryResults(results);
|
|
2130
|
-
|
|
2158
|
+
const rowCount = extractAffectedRows(results);
|
|
2159
|
+
return { rows, rowCount };
|
|
2131
2160
|
} catch (error) {
|
|
2132
2161
|
console.error("Error executing query:", error);
|
|
2133
2162
|
throw error;
|
|
@@ -2274,6 +2303,27 @@ function getClientIdentifier(extra) {
|
|
|
2274
2303
|
return "stdio";
|
|
2275
2304
|
}
|
|
2276
2305
|
|
|
2306
|
+
// src/utils/tool-handler-helpers.ts
|
|
2307
|
+
function getEffectiveSourceId(sourceId) {
|
|
2308
|
+
return sourceId || "default";
|
|
2309
|
+
}
|
|
2310
|
+
function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
|
|
2311
|
+
return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
|
|
2312
|
+
}
|
|
2313
|
+
function trackToolRequest(metadata, startTime, extra, success, error) {
|
|
2314
|
+
requestStore.add({
|
|
2315
|
+
id: crypto.randomUUID(),
|
|
2316
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2317
|
+
sourceId: metadata.sourceId,
|
|
2318
|
+
toolName: metadata.toolName,
|
|
2319
|
+
sql: metadata.sql,
|
|
2320
|
+
durationMs: Date.now() - startTime,
|
|
2321
|
+
client: getClientIdentifier(extra),
|
|
2322
|
+
success,
|
|
2323
|
+
error
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2277
2327
|
// src/tools/execute-sql.ts
|
|
2278
2328
|
var executeSqlSchema = {
|
|
2279
2329
|
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
@@ -2289,7 +2339,7 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
2289
2339
|
return async (args, extra) => {
|
|
2290
2340
|
const { sql: sql2 } = args;
|
|
2291
2341
|
const startTime = Date.now();
|
|
2292
|
-
const effectiveSourceId = sourceId
|
|
2342
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
2293
2343
|
let success = true;
|
|
2294
2344
|
let errorMessage;
|
|
2295
2345
|
let result;
|
|
@@ -2300,18 +2350,18 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
2300
2350
|
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
|
|
2301
2351
|
const isReadonly = toolConfig?.readonly === true;
|
|
2302
2352
|
if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
|
|
2303
|
-
errorMessage = `Read-only mode is enabled
|
|
2353
|
+
errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
2304
2354
|
success = false;
|
|
2305
2355
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
2306
2356
|
}
|
|
2307
2357
|
const executeOptions = {
|
|
2308
|
-
readonly:
|
|
2309
|
-
|
|
2358
|
+
readonly: toolConfig?.readonly,
|
|
2359
|
+
maxRows: toolConfig?.max_rows
|
|
2310
2360
|
};
|
|
2311
2361
|
result = await connector.executeSQL(sql2, executeOptions);
|
|
2312
2362
|
const responseData = {
|
|
2313
2363
|
rows: result.rows,
|
|
2314
|
-
count: result.
|
|
2364
|
+
count: result.rowCount,
|
|
2315
2365
|
source_id: effectiveSourceId
|
|
2316
2366
|
};
|
|
2317
2367
|
return createToolSuccessResponse(responseData);
|
|
@@ -2320,17 +2370,17 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
2320
2370
|
errorMessage = error.message;
|
|
2321
2371
|
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
2322
2372
|
} finally {
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2373
|
+
trackToolRequest(
|
|
2374
|
+
{
|
|
2375
|
+
sourceId: effectiveSourceId,
|
|
2376
|
+
toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
|
|
2377
|
+
sql: sql2
|
|
2378
|
+
},
|
|
2379
|
+
startTime,
|
|
2380
|
+
extra,
|
|
2331
2381
|
success,
|
|
2332
|
-
|
|
2333
|
-
|
|
2382
|
+
errorMessage
|
|
2383
|
+
);
|
|
2334
2384
|
}
|
|
2335
2385
|
};
|
|
2336
2386
|
}
|
|
@@ -2617,7 +2667,7 @@ async function searchIndexes(connector, pattern, schemaFilter, tableFilter, deta
|
|
|
2617
2667
|
return results;
|
|
2618
2668
|
}
|
|
2619
2669
|
function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
2620
|
-
return async (args,
|
|
2670
|
+
return async (args, extra) => {
|
|
2621
2671
|
const {
|
|
2622
2672
|
object_type,
|
|
2623
2673
|
pattern = "%",
|
|
@@ -2626,29 +2676,30 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2626
2676
|
detail_level = "names",
|
|
2627
2677
|
limit = 100
|
|
2628
2678
|
} = args;
|
|
2679
|
+
const startTime = Date.now();
|
|
2680
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
2681
|
+
let success = true;
|
|
2682
|
+
let errorMessage;
|
|
2629
2683
|
try {
|
|
2630
2684
|
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
2631
2685
|
if (table) {
|
|
2632
2686
|
if (!schema) {
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
);
|
|
2687
|
+
success = false;
|
|
2688
|
+
errorMessage = "The 'table' parameter requires 'schema' to be specified";
|
|
2689
|
+
return createToolErrorResponse(errorMessage, "SCHEMA_REQUIRED");
|
|
2637
2690
|
}
|
|
2638
2691
|
if (!["column", "index"].includes(object_type)) {
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
);
|
|
2692
|
+
success = false;
|
|
2693
|
+
errorMessage = `The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`;
|
|
2694
|
+
return createToolErrorResponse(errorMessage, "INVALID_TABLE_FILTER");
|
|
2643
2695
|
}
|
|
2644
2696
|
}
|
|
2645
2697
|
if (schema) {
|
|
2646
2698
|
const schemas = await connector.getSchemas();
|
|
2647
2699
|
if (!schemas.includes(schema)) {
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
);
|
|
2700
|
+
success = false;
|
|
2701
|
+
errorMessage = `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`;
|
|
2702
|
+
return createToolErrorResponse(errorMessage, "SCHEMA_NOT_FOUND");
|
|
2652
2703
|
}
|
|
2653
2704
|
}
|
|
2654
2705
|
let results = [];
|
|
@@ -2669,10 +2720,9 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2669
2720
|
results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
|
|
2670
2721
|
break;
|
|
2671
2722
|
default:
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
);
|
|
2723
|
+
success = false;
|
|
2724
|
+
errorMessage = `Unsupported object_type: ${object_type}`;
|
|
2725
|
+
return createToolErrorResponse(errorMessage, "INVALID_OBJECT_TYPE");
|
|
2676
2726
|
}
|
|
2677
2727
|
return createToolSuccessResponse({
|
|
2678
2728
|
object_type,
|
|
@@ -2685,10 +2735,24 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
2685
2735
|
truncated: results.length === limit
|
|
2686
2736
|
});
|
|
2687
2737
|
} catch (error) {
|
|
2738
|
+
success = false;
|
|
2739
|
+
errorMessage = error.message;
|
|
2688
2740
|
return createToolErrorResponse(
|
|
2689
|
-
`Error searching database objects: ${
|
|
2741
|
+
`Error searching database objects: ${errorMessage}`,
|
|
2690
2742
|
"SEARCH_ERROR"
|
|
2691
2743
|
);
|
|
2744
|
+
} finally {
|
|
2745
|
+
trackToolRequest(
|
|
2746
|
+
{
|
|
2747
|
+
sourceId: effectiveSourceId,
|
|
2748
|
+
toolName: effectiveSourceId === "default" ? "search_objects" : `search_objects_${effectiveSourceId}`,
|
|
2749
|
+
sql: `search_objects(object_type=${object_type}, pattern=${pattern}, schema=${schema || "all"}, table=${table || "all"}, detail_level=${detail_level})`
|
|
2750
|
+
},
|
|
2751
|
+
startTime,
|
|
2752
|
+
extra,
|
|
2753
|
+
success,
|
|
2754
|
+
errorMessage
|
|
2755
|
+
);
|
|
2692
2756
|
}
|
|
2693
2757
|
};
|
|
2694
2758
|
}
|
|
@@ -2731,9 +2795,14 @@ function zodToParameters(schema) {
|
|
|
2731
2795
|
function getExecuteSqlMetadata(sourceId) {
|
|
2732
2796
|
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
2733
2797
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
2734
|
-
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
|
|
2735
2798
|
const dbType = sourceConfig.type;
|
|
2736
2799
|
const isSingleSource = sourceIds.length === 1;
|
|
2800
|
+
const registry = getToolRegistry();
|
|
2801
|
+
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
|
|
2802
|
+
const executeOptions = {
|
|
2803
|
+
readonly: toolConfig?.readonly,
|
|
2804
|
+
maxRows: toolConfig?.max_rows
|
|
2805
|
+
};
|
|
2737
2806
|
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
2738
2807
|
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
2739
2808
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
@@ -2783,13 +2852,17 @@ function customParamsToToolParams(params) {
|
|
|
2783
2852
|
description: param.description
|
|
2784
2853
|
}));
|
|
2785
2854
|
}
|
|
2786
|
-
function buildExecuteSqlTool(sourceId) {
|
|
2855
|
+
function buildExecuteSqlTool(sourceId, toolConfig) {
|
|
2787
2856
|
const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
|
|
2788
2857
|
const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
|
|
2858
|
+
const readonly = toolConfig && "readonly" in toolConfig ? toolConfig.readonly : void 0;
|
|
2859
|
+
const max_rows = toolConfig && "max_rows" in toolConfig ? toolConfig.max_rows : void 0;
|
|
2789
2860
|
return {
|
|
2790
2861
|
name: executeSqlMetadata.name,
|
|
2791
2862
|
description: executeSqlMetadata.description,
|
|
2792
|
-
parameters: executeSqlParameters
|
|
2863
|
+
parameters: executeSqlParameters,
|
|
2864
|
+
readonly,
|
|
2865
|
+
max_rows
|
|
2793
2866
|
};
|
|
2794
2867
|
}
|
|
2795
2868
|
function buildSearchObjectsTool(sourceId) {
|
|
@@ -2834,14 +2907,19 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
2834
2907
|
required: false,
|
|
2835
2908
|
description: "Max results (default: 100, max: 1000)"
|
|
2836
2909
|
}
|
|
2837
|
-
]
|
|
2910
|
+
],
|
|
2911
|
+
readonly: true
|
|
2912
|
+
// search_objects is always readonly
|
|
2838
2913
|
};
|
|
2839
2914
|
}
|
|
2840
2915
|
function buildCustomTool(toolConfig) {
|
|
2841
2916
|
return {
|
|
2842
2917
|
name: toolConfig.name,
|
|
2843
2918
|
description: toolConfig.description,
|
|
2844
|
-
parameters: customParamsToToolParams(toolConfig.parameters)
|
|
2919
|
+
parameters: customParamsToToolParams(toolConfig.parameters),
|
|
2920
|
+
statement: toolConfig.statement,
|
|
2921
|
+
readonly: toolConfig.readonly,
|
|
2922
|
+
max_rows: toolConfig.max_rows
|
|
2845
2923
|
};
|
|
2846
2924
|
}
|
|
2847
2925
|
function getToolsForSource(sourceId) {
|
|
@@ -2849,7 +2927,7 @@ function getToolsForSource(sourceId) {
|
|
|
2849
2927
|
const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
|
|
2850
2928
|
return enabledToolConfigs.map((toolConfig) => {
|
|
2851
2929
|
if (toolConfig.name === "execute_sql") {
|
|
2852
|
-
return buildExecuteSqlTool(sourceId);
|
|
2930
|
+
return buildExecuteSqlTool(sourceId, toolConfig);
|
|
2853
2931
|
} else if (toolConfig.name === "search_objects") {
|
|
2854
2932
|
return buildSearchObjectsTool(sourceId);
|
|
2855
2933
|
} else {
|
|
@@ -2916,12 +2994,13 @@ function createCustomToolHandler(toolConfig) {
|
|
|
2916
2994
|
try {
|
|
2917
2995
|
const validatedArgs = zodSchema.parse(args);
|
|
2918
2996
|
const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
|
|
2919
|
-
const executeOptions =
|
|
2920
|
-
toolConfig.
|
|
2921
|
-
|
|
2997
|
+
const executeOptions = {
|
|
2998
|
+
readonly: toolConfig.readonly,
|
|
2999
|
+
maxRows: toolConfig.max_rows
|
|
3000
|
+
};
|
|
2922
3001
|
const isReadonly = executeOptions.readonly === true;
|
|
2923
3002
|
if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
|
|
2924
|
-
errorMessage =
|
|
3003
|
+
errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
|
|
2925
3004
|
success = false;
|
|
2926
3005
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
2927
3006
|
}
|
|
@@ -2936,7 +3015,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
2936
3015
|
);
|
|
2937
3016
|
const responseData = {
|
|
2938
3017
|
rows: result.rows,
|
|
2939
|
-
count: result.
|
|
3018
|
+
count: result.rowCount,
|
|
2940
3019
|
source_id: toolConfig.source
|
|
2941
3020
|
};
|
|
2942
3021
|
return createToolSuccessResponse(responseData);
|
|
@@ -2954,17 +3033,17 @@ Parameters: ${JSON.stringify(paramValues)}`;
|
|
|
2954
3033
|
}
|
|
2955
3034
|
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
2956
3035
|
} finally {
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3036
|
+
trackToolRequest(
|
|
3037
|
+
{
|
|
3038
|
+
sourceId: toolConfig.source,
|
|
3039
|
+
toolName: toolConfig.name,
|
|
3040
|
+
sql: toolConfig.statement
|
|
3041
|
+
},
|
|
3042
|
+
startTime,
|
|
3043
|
+
extra,
|
|
2965
3044
|
success,
|
|
2966
|
-
|
|
2967
|
-
|
|
3045
|
+
errorMessage
|
|
3046
|
+
);
|
|
2968
3047
|
}
|
|
2969
3048
|
};
|
|
2970
3049
|
}
|
|
@@ -3039,7 +3118,6 @@ function registerCustomTool(server, sourceId, toolConfig) {
|
|
|
3039
3118
|
},
|
|
3040
3119
|
createCustomToolHandler(toolConfig)
|
|
3041
3120
|
);
|
|
3042
|
-
console.error(` - ${toolConfig.name} \u2192 ${toolConfig.source} (${dbType})`);
|
|
3043
3121
|
}
|
|
3044
3122
|
|
|
3045
3123
|
// src/api/sources.ts
|
|
@@ -3069,12 +3147,6 @@ function transformSourceConfig(source) {
|
|
|
3069
3147
|
if (source.user) {
|
|
3070
3148
|
dataSource.user = source.user;
|
|
3071
3149
|
}
|
|
3072
|
-
if (source.readonly !== void 0) {
|
|
3073
|
-
dataSource.readonly = source.readonly;
|
|
3074
|
-
}
|
|
3075
|
-
if (source.max_rows !== void 0) {
|
|
3076
|
-
dataSource.max_rows = source.max_rows;
|
|
3077
|
-
}
|
|
3078
3150
|
if (source.ssh_host) {
|
|
3079
3151
|
const sshTunnel = {
|
|
3080
3152
|
enabled: true,
|
|
@@ -3314,7 +3386,7 @@ See documentation for more details on configuring database connections.
|
|
|
3314
3386
|
const sources = sourceConfigsData.sources;
|
|
3315
3387
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
3316
3388
|
await connectorManager.connectWithSources(sources);
|
|
3317
|
-
const { initializeToolRegistry } = await import("./registry-
|
|
3389
|
+
const { initializeToolRegistry } = await import("./registry-XXEL5IXH.js");
|
|
3318
3390
|
initializeToolRegistry({
|
|
3319
3391
|
sources: sourceConfigsData.sources,
|
|
3320
3392
|
tools: sourceConfigsData.tools
|
|
@@ -3343,7 +3415,7 @@ See documentation for more details on configuring database connections.
|
|
|
3343
3415
|
console.error(generateBanner(SERVER_VERSION, activeModes));
|
|
3344
3416
|
const sourceDisplayInfos = buildSourceDisplayInfo(
|
|
3345
3417
|
sources,
|
|
3346
|
-
(sourceId) => getToolsForSource(sourceId).map((t) => t.name),
|
|
3418
|
+
(sourceId) => getToolsForSource(sourceId).map((t) => t.readonly ? `\u{1F512} ${t.name}` : t.name),
|
|
3347
3419
|
isDemo
|
|
3348
3420
|
);
|
|
3349
3421
|
console.error(generateStartupTable(sourceDisplayInfos));
|
|
@@ -3404,11 +3476,11 @@ See documentation for more details on configuring database connections.
|
|
|
3404
3476
|
app.listen(port, "0.0.0.0", () => {
|
|
3405
3477
|
if (process.env.NODE_ENV === "development") {
|
|
3406
3478
|
console.error("Development mode detected!");
|
|
3407
|
-
console.error("
|
|
3479
|
+
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
3408
3480
|
console.error(" Backend API: http://localhost:8080");
|
|
3409
3481
|
console.error("");
|
|
3410
3482
|
} else {
|
|
3411
|
-
console.error(`
|
|
3483
|
+
console.error(`Workbench at http://localhost:${port}/`);
|
|
3412
3484
|
}
|
|
3413
3485
|
console.error(`MCP server endpoint at http://localhost:${port}/mcp`);
|
|
3414
3486
|
});
|