@bytebase/dbhub 0.11.10 → 0.12.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 +33 -24
- package/dist/index.js +753 -77
- package/dist/public/assets/index-C-kOl-8S.css +1 -0
- package/dist/public/assets/index-CDt2HpUt.js +51 -0
- package/dist/public/index.html +2 -2
- package/package.json +3 -3
- package/dist/public/assets/index--LC9Foha.css +0 -1
- package/dist/public/assets/index-BZTaHJm6.js +0 -51
package/dist/index.js
CHANGED
|
@@ -149,38 +149,82 @@ var SafeURL = class {
|
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
// src/utils/dsn-obfuscate.ts
|
|
152
|
+
function parseConnectionInfoFromDSN(dsn) {
|
|
153
|
+
if (!dsn) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const type = getDatabaseTypeFromDSN(dsn);
|
|
158
|
+
if (typeof type === "undefined") {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (type === "sqlite") {
|
|
162
|
+
const prefix = "sqlite:///";
|
|
163
|
+
if (dsn.length > prefix.length) {
|
|
164
|
+
const rawPath = dsn.substring(prefix.length);
|
|
165
|
+
const firstChar = rawPath[0];
|
|
166
|
+
const isWindowsDrive = rawPath.length > 1 && rawPath[1] === ":";
|
|
167
|
+
const isSpecialPath = firstChar === ":" || firstChar === "." || firstChar === "~" || isWindowsDrive;
|
|
168
|
+
return {
|
|
169
|
+
type,
|
|
170
|
+
database: isSpecialPath ? rawPath : "/" + rawPath
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { type };
|
|
174
|
+
}
|
|
175
|
+
const url = new SafeURL(dsn);
|
|
176
|
+
const info = { type };
|
|
177
|
+
if (url.hostname) {
|
|
178
|
+
info.host = url.hostname;
|
|
179
|
+
}
|
|
180
|
+
if (url.port) {
|
|
181
|
+
info.port = parseInt(url.port, 10);
|
|
182
|
+
}
|
|
183
|
+
if (url.pathname && url.pathname.length > 1) {
|
|
184
|
+
info.database = url.pathname.substring(1);
|
|
185
|
+
}
|
|
186
|
+
if (url.username) {
|
|
187
|
+
info.user = url.username;
|
|
188
|
+
}
|
|
189
|
+
return info;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
152
194
|
function obfuscateDSNPassword(dsn) {
|
|
153
195
|
if (!dsn) {
|
|
154
196
|
return dsn;
|
|
155
197
|
}
|
|
156
198
|
try {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
199
|
+
const type = getDatabaseTypeFromDSN(dsn);
|
|
200
|
+
if (type === "sqlite") {
|
|
159
201
|
return dsn;
|
|
160
202
|
}
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
203
|
+
const url = new SafeURL(dsn);
|
|
204
|
+
if (!url.password) {
|
|
163
205
|
return dsn;
|
|
164
206
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
207
|
+
const obfuscatedPassword = "*".repeat(Math.min(url.password.length, 8));
|
|
208
|
+
const protocol = dsn.split(":")[0];
|
|
209
|
+
let result;
|
|
210
|
+
if (url.username) {
|
|
211
|
+
result = `${protocol}://${url.username}:${obfuscatedPassword}@${url.hostname}`;
|
|
212
|
+
} else {
|
|
213
|
+
result = `${protocol}://${obfuscatedPassword}@${url.hostname}`;
|
|
168
214
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return dsn;
|
|
215
|
+
if (url.port) {
|
|
216
|
+
result += `:${url.port}`;
|
|
172
217
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
218
|
+
result += url.pathname;
|
|
219
|
+
if (url.searchParams.size > 0) {
|
|
220
|
+
const params = [];
|
|
221
|
+
url.forEachSearchParam((value, key) => {
|
|
222
|
+
params.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
223
|
+
});
|
|
224
|
+
result += `?${params.join("&")}`;
|
|
178
225
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const obfuscatedPassword = "*".repeat(Math.min(password.length, 8));
|
|
182
|
-
return `${protocol}://${username}:${obfuscatedPassword}@${hostPart}`;
|
|
183
|
-
} catch (error) {
|
|
226
|
+
return result;
|
|
227
|
+
} catch {
|
|
184
228
|
return dsn;
|
|
185
229
|
}
|
|
186
230
|
}
|
|
@@ -189,7 +233,10 @@ function getDatabaseTypeFromDSN(dsn) {
|
|
|
189
233
|
return void 0;
|
|
190
234
|
}
|
|
191
235
|
const protocol = dsn.split(":")[0];
|
|
192
|
-
|
|
236
|
+
return protocolToConnectorType(protocol);
|
|
237
|
+
}
|
|
238
|
+
function protocolToConnectorType(protocol) {
|
|
239
|
+
const mapping = {
|
|
193
240
|
"postgres": "postgres",
|
|
194
241
|
"postgresql": "postgres",
|
|
195
242
|
"mysql": "mysql",
|
|
@@ -197,7 +244,17 @@ function getDatabaseTypeFromDSN(dsn) {
|
|
|
197
244
|
"sqlserver": "sqlserver",
|
|
198
245
|
"sqlite": "sqlite"
|
|
199
246
|
};
|
|
200
|
-
return
|
|
247
|
+
return mapping[protocol];
|
|
248
|
+
}
|
|
249
|
+
function getDefaultPortForType(type) {
|
|
250
|
+
const ports = {
|
|
251
|
+
"postgres": 5432,
|
|
252
|
+
"mysql": 3306,
|
|
253
|
+
"mariadb": 3306,
|
|
254
|
+
"sqlserver": 1433,
|
|
255
|
+
"sqlite": void 0
|
|
256
|
+
};
|
|
257
|
+
return ports[type];
|
|
201
258
|
}
|
|
202
259
|
|
|
203
260
|
// src/utils/sql-row-limiter.ts
|
|
@@ -360,6 +417,9 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
360
417
|
async connect(dsn, initScript, config) {
|
|
361
418
|
try {
|
|
362
419
|
const poolConfig = await this.dsnParser.parse(dsn, config);
|
|
420
|
+
if (config?.readonly) {
|
|
421
|
+
poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
|
|
422
|
+
}
|
|
363
423
|
this.pool = new Pool(poolConfig);
|
|
364
424
|
const client = await this.pool.connect();
|
|
365
425
|
console.error("Successfully connected to PostgreSQL database");
|
|
@@ -1009,6 +1069,38 @@ ConnectorRegistry.register(sqlServerConnector);
|
|
|
1009
1069
|
|
|
1010
1070
|
// src/connectors/sqlite/index.ts
|
|
1011
1071
|
import Database from "better-sqlite3";
|
|
1072
|
+
|
|
1073
|
+
// src/utils/identifier-quoter.ts
|
|
1074
|
+
function quoteIdentifier(identifier, dbType) {
|
|
1075
|
+
if (/[\0\x08\x09\x1a\n\r]/.test(identifier)) {
|
|
1076
|
+
throw new Error(`Invalid identifier: contains control characters: ${identifier}`);
|
|
1077
|
+
}
|
|
1078
|
+
if (!identifier) {
|
|
1079
|
+
throw new Error("Identifier cannot be empty");
|
|
1080
|
+
}
|
|
1081
|
+
switch (dbType) {
|
|
1082
|
+
case "postgres":
|
|
1083
|
+
case "sqlite":
|
|
1084
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1085
|
+
case "mysql":
|
|
1086
|
+
case "mariadb":
|
|
1087
|
+
return `\`${identifier.replace(/`/g, "``")}\``;
|
|
1088
|
+
case "sqlserver":
|
|
1089
|
+
return `[${identifier.replace(/]/g, "]]")}]`;
|
|
1090
|
+
default:
|
|
1091
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function quoteQualifiedIdentifier(tableName, schemaName, dbType) {
|
|
1095
|
+
const quotedTable = quoteIdentifier(tableName, dbType);
|
|
1096
|
+
if (schemaName) {
|
|
1097
|
+
const quotedSchema = quoteIdentifier(schemaName, dbType);
|
|
1098
|
+
return `${quotedSchema}.${quotedTable}`;
|
|
1099
|
+
}
|
|
1100
|
+
return quotedTable;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/connectors/sqlite/index.ts
|
|
1012
1104
|
var SQLiteDSNParser = class {
|
|
1013
1105
|
async parse(dsn, config) {
|
|
1014
1106
|
if (!this.isValidDSN(dsn)) {
|
|
@@ -1073,7 +1165,11 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1073
1165
|
const parsedConfig = await this.dsnParser.parse(dsn, config);
|
|
1074
1166
|
this.dbPath = parsedConfig.dbPath;
|
|
1075
1167
|
try {
|
|
1076
|
-
|
|
1168
|
+
const dbOptions = {};
|
|
1169
|
+
if (config?.readonly && this.dbPath !== ":memory:") {
|
|
1170
|
+
dbOptions.readonly = true;
|
|
1171
|
+
}
|
|
1172
|
+
this.db = new Database(this.dbPath, dbOptions);
|
|
1077
1173
|
console.error("Successfully connected to SQLite database");
|
|
1078
1174
|
if (initScript) {
|
|
1079
1175
|
this.db.exec(initScript);
|
|
@@ -1158,16 +1254,18 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1158
1254
|
AND tbl_name = ?
|
|
1159
1255
|
`
|
|
1160
1256
|
).all(tableName);
|
|
1161
|
-
const
|
|
1257
|
+
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
1258
|
+
const indexListRows = this.db.prepare(`PRAGMA index_list(${quotedTableName})`).all();
|
|
1162
1259
|
const indexUniqueMap = /* @__PURE__ */ new Map();
|
|
1163
1260
|
for (const indexListRow of indexListRows) {
|
|
1164
1261
|
indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
|
|
1165
1262
|
}
|
|
1166
|
-
const tableInfo = this.db.prepare(`PRAGMA table_info(${
|
|
1263
|
+
const tableInfo = this.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
|
|
1167
1264
|
const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
|
|
1168
1265
|
const results = [];
|
|
1169
1266
|
for (const indexInfo of indexInfoRows) {
|
|
1170
|
-
const
|
|
1267
|
+
const quotedIndexName = quoteIdentifier(indexInfo.index_name, "sqlite");
|
|
1268
|
+
const indexDetailRows = this.db.prepare(`PRAGMA index_info(${quotedIndexName})`).all();
|
|
1171
1269
|
const columnNames = indexDetailRows.map((row) => row.name);
|
|
1172
1270
|
results.push({
|
|
1173
1271
|
index_name: indexInfo.index_name,
|
|
@@ -1194,7 +1292,8 @@ var SQLiteConnector = class _SQLiteConnector {
|
|
|
1194
1292
|
throw new Error("Not connected to SQLite database");
|
|
1195
1293
|
}
|
|
1196
1294
|
try {
|
|
1197
|
-
const
|
|
1295
|
+
const quotedTableName = quoteIdentifier(tableName, "sqlite");
|
|
1296
|
+
const rows = this.db.prepare(`PRAGMA table_info(${quotedTableName})`).all();
|
|
1198
1297
|
const columns = rows.map((row) => ({
|
|
1199
1298
|
column_name: row.name,
|
|
1200
1299
|
data_type: row.type,
|
|
@@ -2629,6 +2728,17 @@ async function resolveSourceConfigs() {
|
|
|
2629
2728
|
if (!isDemoMode()) {
|
|
2630
2729
|
const tomlConfig = loadTomlConfig();
|
|
2631
2730
|
if (tomlConfig) {
|
|
2731
|
+
const idData = resolveId();
|
|
2732
|
+
if (idData) {
|
|
2733
|
+
throw new Error(
|
|
2734
|
+
"The --id flag cannot be used with TOML configuration. TOML config defines source IDs directly. Either remove the --id flag or use command-line DSN configuration instead."
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
if (isReadOnlyMode()) {
|
|
2738
|
+
throw new Error(
|
|
2739
|
+
"The --readonly flag cannot be used with TOML configuration. TOML config defines readonly mode per-source using 'readonly = true'. Either remove the --readonly flag or use command-line DSN configuration instead."
|
|
2740
|
+
);
|
|
2741
|
+
}
|
|
2632
2742
|
return tomlConfig;
|
|
2633
2743
|
}
|
|
2634
2744
|
}
|
|
@@ -2657,11 +2767,28 @@ async function resolveSourceConfigs() {
|
|
|
2657
2767
|
} else {
|
|
2658
2768
|
throw new Error(`Unsupported database type in DSN: ${protocol}`);
|
|
2659
2769
|
}
|
|
2770
|
+
const idData = resolveId();
|
|
2771
|
+
const sourceId = idData?.id || "default";
|
|
2660
2772
|
const source = {
|
|
2661
|
-
id:
|
|
2773
|
+
id: sourceId,
|
|
2662
2774
|
type: dbType,
|
|
2663
2775
|
dsn: dsnResult.dsn
|
|
2664
2776
|
};
|
|
2777
|
+
const connectionInfo = parseConnectionInfoFromDSN(dsnResult.dsn);
|
|
2778
|
+
if (connectionInfo) {
|
|
2779
|
+
if (connectionInfo.host) {
|
|
2780
|
+
source.host = connectionInfo.host;
|
|
2781
|
+
}
|
|
2782
|
+
if (connectionInfo.port !== void 0) {
|
|
2783
|
+
source.port = connectionInfo.port;
|
|
2784
|
+
}
|
|
2785
|
+
if (connectionInfo.database) {
|
|
2786
|
+
source.database = connectionInfo.database;
|
|
2787
|
+
}
|
|
2788
|
+
if (connectionInfo.user) {
|
|
2789
|
+
source.user = connectionInfo.user;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2665
2792
|
const sshResult = resolveSSHConfig();
|
|
2666
2793
|
if (sshResult) {
|
|
2667
2794
|
source.ssh_host = sshResult.config.host;
|
|
@@ -2832,6 +2959,26 @@ function processSourceConfigs(sources, configPath) {
|
|
|
2832
2959
|
if (processed.dsn && processed.dsn.startsWith("sqlite:///~")) {
|
|
2833
2960
|
processed.dsn = `sqlite:///${expandHomeDir(processed.dsn.substring(11))}`;
|
|
2834
2961
|
}
|
|
2962
|
+
if (processed.dsn) {
|
|
2963
|
+
const connectionInfo = parseConnectionInfoFromDSN(processed.dsn);
|
|
2964
|
+
if (connectionInfo) {
|
|
2965
|
+
if (!processed.type && connectionInfo.type) {
|
|
2966
|
+
processed.type = connectionInfo.type;
|
|
2967
|
+
}
|
|
2968
|
+
if (!processed.host && connectionInfo.host) {
|
|
2969
|
+
processed.host = connectionInfo.host;
|
|
2970
|
+
}
|
|
2971
|
+
if (processed.port === void 0 && connectionInfo.port !== void 0) {
|
|
2972
|
+
processed.port = connectionInfo.port;
|
|
2973
|
+
}
|
|
2974
|
+
if (!processed.database && connectionInfo.database) {
|
|
2975
|
+
processed.database = connectionInfo.database;
|
|
2976
|
+
}
|
|
2977
|
+
if (!processed.user && connectionInfo.user) {
|
|
2978
|
+
processed.user = connectionInfo.user;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2835
2982
|
return processed;
|
|
2836
2983
|
});
|
|
2837
2984
|
}
|
|
@@ -2863,7 +3010,7 @@ function buildDSNFromSource(source) {
|
|
|
2863
3010
|
`Source '${source.id}': missing required connection parameters. Required: type, host, user, password, database`
|
|
2864
3011
|
);
|
|
2865
3012
|
}
|
|
2866
|
-
const port = source.port || (source.type
|
|
3013
|
+
const port = source.port || getDefaultPortForType(source.type);
|
|
2867
3014
|
if (!port) {
|
|
2868
3015
|
throw new Error(`Source '${source.id}': unable to determine port`);
|
|
2869
3016
|
}
|
|
@@ -2963,6 +3110,9 @@ var ConnectorManager = class {
|
|
|
2963
3110
|
if (connector.id === "sqlserver" && source.request_timeout !== void 0) {
|
|
2964
3111
|
config.requestTimeoutSeconds = source.request_timeout;
|
|
2965
3112
|
}
|
|
3113
|
+
if (source.readonly !== void 0) {
|
|
3114
|
+
config.readonly = source.readonly;
|
|
3115
|
+
}
|
|
2966
3116
|
await connector.connect(actualDSN, source.init_script, config);
|
|
2967
3117
|
this.connectors.set(sourceId, connector);
|
|
2968
3118
|
this.sourceIds.push(sourceId);
|
|
@@ -3114,16 +3264,11 @@ var ConnectorManager = class {
|
|
|
3114
3264
|
* Get default port for a database based on DSN protocol
|
|
3115
3265
|
*/
|
|
3116
3266
|
getDefaultPort(dsn) {
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
return 3306;
|
|
3121
|
-
} else if (dsn.startsWith("mariadb://")) {
|
|
3122
|
-
return 3306;
|
|
3123
|
-
} else if (dsn.startsWith("sqlserver://")) {
|
|
3124
|
-
return 1433;
|
|
3267
|
+
const type = getDatabaseTypeFromDSN(dsn);
|
|
3268
|
+
if (!type) {
|
|
3269
|
+
return 0;
|
|
3125
3270
|
}
|
|
3126
|
-
return 0;
|
|
3271
|
+
return getDefaultPortForType(type) ?? 0;
|
|
3127
3272
|
}
|
|
3128
3273
|
};
|
|
3129
3274
|
|
|
@@ -3511,10 +3656,66 @@ var allowedKeywords = {
|
|
|
3511
3656
|
sqlserver: ["select", "with", "explain", "showplan"]
|
|
3512
3657
|
};
|
|
3513
3658
|
|
|
3659
|
+
// src/requests/store.ts
|
|
3660
|
+
var RequestStore = class {
|
|
3661
|
+
constructor() {
|
|
3662
|
+
this.store = /* @__PURE__ */ new Map();
|
|
3663
|
+
this.maxPerSource = 100;
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Add a request to the store
|
|
3667
|
+
* Evicts oldest entry if at capacity
|
|
3668
|
+
*/
|
|
3669
|
+
add(request) {
|
|
3670
|
+
const requests = this.store.get(request.sourceId) ?? [];
|
|
3671
|
+
requests.push(request);
|
|
3672
|
+
if (requests.length > this.maxPerSource) {
|
|
3673
|
+
requests.shift();
|
|
3674
|
+
}
|
|
3675
|
+
this.store.set(request.sourceId, requests);
|
|
3676
|
+
}
|
|
3677
|
+
/**
|
|
3678
|
+
* Get requests, optionally filtered by source
|
|
3679
|
+
* Returns newest first
|
|
3680
|
+
*/
|
|
3681
|
+
getAll(sourceId) {
|
|
3682
|
+
let requests;
|
|
3683
|
+
if (sourceId) {
|
|
3684
|
+
requests = [...this.store.get(sourceId) ?? []];
|
|
3685
|
+
} else {
|
|
3686
|
+
requests = Array.from(this.store.values()).flat();
|
|
3687
|
+
}
|
|
3688
|
+
return requests.sort(
|
|
3689
|
+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
3690
|
+
);
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Get total count of requests across all sources
|
|
3694
|
+
*/
|
|
3695
|
+
getTotal() {
|
|
3696
|
+
return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
|
|
3697
|
+
}
|
|
3698
|
+
/**
|
|
3699
|
+
* Clear all requests (useful for testing)
|
|
3700
|
+
*/
|
|
3701
|
+
clear() {
|
|
3702
|
+
this.store.clear();
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
3705
|
+
|
|
3706
|
+
// src/requests/index.ts
|
|
3707
|
+
var requestStore = new RequestStore();
|
|
3708
|
+
|
|
3514
3709
|
// src/tools/execute-sql.ts
|
|
3710
|
+
function getClientIdentifier(extra) {
|
|
3711
|
+
const userAgent = extra?.requestInfo?.headers?.["user-agent"];
|
|
3712
|
+
if (userAgent) {
|
|
3713
|
+
return userAgent;
|
|
3714
|
+
}
|
|
3715
|
+
return "stdio";
|
|
3716
|
+
}
|
|
3515
3717
|
var executeSqlSchema = {
|
|
3516
|
-
sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
|
|
3517
|
-
source_id: z.string().optional().describe("Database source ID to execute the query against (optional, defaults to first configured source)")
|
|
3718
|
+
sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
|
|
3518
3719
|
};
|
|
3519
3720
|
function splitSQLStatements(sql2) {
|
|
3520
3721
|
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
@@ -3540,45 +3741,506 @@ function areAllStatementsReadOnly(sql2, connectorType) {
|
|
|
3540
3741
|
const statements = splitSQLStatements(sql2);
|
|
3541
3742
|
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
3542
3743
|
}
|
|
3543
|
-
|
|
3744
|
+
function createExecuteSqlToolHandler(sourceId) {
|
|
3745
|
+
return async (args, extra) => {
|
|
3746
|
+
const { sql: sql2 } = args;
|
|
3747
|
+
const startTime = Date.now();
|
|
3748
|
+
const effectiveSourceId = sourceId || "default";
|
|
3749
|
+
let success = true;
|
|
3750
|
+
let errorMessage;
|
|
3751
|
+
let result;
|
|
3752
|
+
try {
|
|
3753
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
3754
|
+
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
|
|
3755
|
+
const isReadonly = executeOptions.readonly === true;
|
|
3756
|
+
if (isReadonly && !areAllStatementsReadOnly(sql2, connector.id)) {
|
|
3757
|
+
errorMessage = `Read-only mode is enabled for source '${effectiveSourceId}'. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
3758
|
+
success = false;
|
|
3759
|
+
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
3760
|
+
}
|
|
3761
|
+
result = await connector.executeSQL(sql2, executeOptions);
|
|
3762
|
+
const responseData = {
|
|
3763
|
+
rows: result.rows,
|
|
3764
|
+
count: result.rows.length,
|
|
3765
|
+
source_id: effectiveSourceId
|
|
3766
|
+
};
|
|
3767
|
+
return createToolSuccessResponse(responseData);
|
|
3768
|
+
} catch (error) {
|
|
3769
|
+
success = false;
|
|
3770
|
+
errorMessage = error.message;
|
|
3771
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
3772
|
+
} finally {
|
|
3773
|
+
requestStore.add({
|
|
3774
|
+
id: crypto.randomUUID(),
|
|
3775
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3776
|
+
sourceId: effectiveSourceId,
|
|
3777
|
+
toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
|
|
3778
|
+
sql: sql2,
|
|
3779
|
+
durationMs: Date.now() - startTime,
|
|
3780
|
+
client: getClientIdentifier(extra),
|
|
3781
|
+
success,
|
|
3782
|
+
error: errorMessage
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
// src/tools/search-objects.ts
|
|
3789
|
+
import { z as z2 } from "zod";
|
|
3790
|
+
var searchDatabaseObjectsSchema = {
|
|
3791
|
+
object_type: z2.enum(["schema", "table", "column", "procedure", "index"]).describe("Type of database object to search for"),
|
|
3792
|
+
pattern: z2.string().optional().default("%").describe("Search pattern (SQL LIKE syntax: % for wildcard, _ for single char). Case-insensitive. Defaults to '%' (match all)."),
|
|
3793
|
+
schema: z2.string().optional().describe("Filter results to a specific schema/database"),
|
|
3794
|
+
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Level of detail to return: names (minimal), summary (with metadata), full (complete structure)"),
|
|
3795
|
+
limit: z2.number().int().positive().max(1e3).default(100).describe("Maximum number of results to return (default: 100, max: 1000)")
|
|
3796
|
+
};
|
|
3797
|
+
function likePatternToRegex(pattern) {
|
|
3798
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
3799
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
3800
|
+
}
|
|
3801
|
+
async function getTableRowCount(connector, tableName, schemaName) {
|
|
3544
3802
|
try {
|
|
3545
|
-
const
|
|
3546
|
-
const
|
|
3547
|
-
|
|
3803
|
+
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
3804
|
+
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
3805
|
+
const result = await connector.executeSQL(countQuery, { maxRows: 1 });
|
|
3806
|
+
if (result.rows && result.rows.length > 0) {
|
|
3807
|
+
return Number(result.rows[0].count || result.rows[0].COUNT || 0);
|
|
3808
|
+
}
|
|
3809
|
+
} catch (error) {
|
|
3810
|
+
return null;
|
|
3811
|
+
}
|
|
3812
|
+
return null;
|
|
3813
|
+
}
|
|
3814
|
+
async function searchSchemas(connector, pattern, detailLevel, limit) {
|
|
3815
|
+
const schemas = await connector.getSchemas();
|
|
3816
|
+
const regex = likePatternToRegex(pattern);
|
|
3817
|
+
const matched = schemas.filter((schema) => regex.test(schema)).slice(0, limit);
|
|
3818
|
+
if (detailLevel === "names") {
|
|
3819
|
+
return matched.map((name) => ({ name }));
|
|
3820
|
+
}
|
|
3821
|
+
const results = await Promise.all(
|
|
3822
|
+
matched.map(async (schemaName) => {
|
|
3823
|
+
try {
|
|
3824
|
+
const tables = await connector.getTables(schemaName);
|
|
3825
|
+
return {
|
|
3826
|
+
name: schemaName,
|
|
3827
|
+
table_count: tables.length
|
|
3828
|
+
};
|
|
3829
|
+
} catch (error) {
|
|
3830
|
+
return {
|
|
3831
|
+
name: schemaName,
|
|
3832
|
+
table_count: 0
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
})
|
|
3836
|
+
);
|
|
3837
|
+
return results;
|
|
3838
|
+
}
|
|
3839
|
+
async function searchTables(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
3840
|
+
const regex = likePatternToRegex(pattern);
|
|
3841
|
+
const results = [];
|
|
3842
|
+
let schemasToSearch;
|
|
3843
|
+
if (schemaFilter) {
|
|
3844
|
+
schemasToSearch = [schemaFilter];
|
|
3845
|
+
} else {
|
|
3846
|
+
schemasToSearch = await connector.getSchemas();
|
|
3847
|
+
}
|
|
3848
|
+
for (const schemaName of schemasToSearch) {
|
|
3849
|
+
if (results.length >= limit) break;
|
|
3850
|
+
try {
|
|
3851
|
+
const tables = await connector.getTables(schemaName);
|
|
3852
|
+
const matched = tables.filter((table) => regex.test(table));
|
|
3853
|
+
for (const tableName of matched) {
|
|
3854
|
+
if (results.length >= limit) break;
|
|
3855
|
+
if (detailLevel === "names") {
|
|
3856
|
+
results.push({
|
|
3857
|
+
name: tableName,
|
|
3858
|
+
schema: schemaName
|
|
3859
|
+
});
|
|
3860
|
+
} else if (detailLevel === "summary") {
|
|
3861
|
+
try {
|
|
3862
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
3863
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
3864
|
+
results.push({
|
|
3865
|
+
name: tableName,
|
|
3866
|
+
schema: schemaName,
|
|
3867
|
+
column_count: columns.length,
|
|
3868
|
+
row_count: rowCount
|
|
3869
|
+
});
|
|
3870
|
+
} catch (error) {
|
|
3871
|
+
results.push({
|
|
3872
|
+
name: tableName,
|
|
3873
|
+
schema: schemaName,
|
|
3874
|
+
column_count: null,
|
|
3875
|
+
row_count: null
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
} else {
|
|
3879
|
+
try {
|
|
3880
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
3881
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
3882
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
3883
|
+
results.push({
|
|
3884
|
+
name: tableName,
|
|
3885
|
+
schema: schemaName,
|
|
3886
|
+
column_count: columns.length,
|
|
3887
|
+
row_count: rowCount,
|
|
3888
|
+
columns: columns.map((col) => ({
|
|
3889
|
+
name: col.column_name,
|
|
3890
|
+
type: col.data_type,
|
|
3891
|
+
nullable: col.is_nullable === "YES",
|
|
3892
|
+
default: col.column_default
|
|
3893
|
+
})),
|
|
3894
|
+
indexes: indexes.map((idx) => ({
|
|
3895
|
+
name: idx.index_name,
|
|
3896
|
+
columns: idx.column_names,
|
|
3897
|
+
unique: idx.is_unique,
|
|
3898
|
+
primary: idx.is_primary
|
|
3899
|
+
}))
|
|
3900
|
+
});
|
|
3901
|
+
} catch (error) {
|
|
3902
|
+
results.push({
|
|
3903
|
+
name: tableName,
|
|
3904
|
+
schema: schemaName,
|
|
3905
|
+
error: `Unable to fetch full details: ${error.message}`
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
} catch (error) {
|
|
3911
|
+
continue;
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
return results;
|
|
3915
|
+
}
|
|
3916
|
+
async function searchColumns(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
3917
|
+
const regex = likePatternToRegex(pattern);
|
|
3918
|
+
const results = [];
|
|
3919
|
+
let schemasToSearch;
|
|
3920
|
+
if (schemaFilter) {
|
|
3921
|
+
schemasToSearch = [schemaFilter];
|
|
3922
|
+
} else {
|
|
3923
|
+
schemasToSearch = await connector.getSchemas();
|
|
3924
|
+
}
|
|
3925
|
+
for (const schemaName of schemasToSearch) {
|
|
3926
|
+
if (results.length >= limit) break;
|
|
3927
|
+
try {
|
|
3928
|
+
const tables = await connector.getTables(schemaName);
|
|
3929
|
+
for (const tableName of tables) {
|
|
3930
|
+
if (results.length >= limit) break;
|
|
3931
|
+
try {
|
|
3932
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
3933
|
+
const matchedColumns = columns.filter((col) => regex.test(col.column_name));
|
|
3934
|
+
for (const column of matchedColumns) {
|
|
3935
|
+
if (results.length >= limit) break;
|
|
3936
|
+
if (detailLevel === "names") {
|
|
3937
|
+
results.push({
|
|
3938
|
+
name: column.column_name,
|
|
3939
|
+
table: tableName,
|
|
3940
|
+
schema: schemaName
|
|
3941
|
+
});
|
|
3942
|
+
} else {
|
|
3943
|
+
results.push({
|
|
3944
|
+
name: column.column_name,
|
|
3945
|
+
table: tableName,
|
|
3946
|
+
schema: schemaName,
|
|
3947
|
+
type: column.data_type,
|
|
3948
|
+
nullable: column.is_nullable === "YES",
|
|
3949
|
+
default: column.column_default
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
} catch (error) {
|
|
3954
|
+
continue;
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
} catch (error) {
|
|
3958
|
+
continue;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return results;
|
|
3962
|
+
}
|
|
3963
|
+
async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
3964
|
+
const regex = likePatternToRegex(pattern);
|
|
3965
|
+
const results = [];
|
|
3966
|
+
let schemasToSearch;
|
|
3967
|
+
if (schemaFilter) {
|
|
3968
|
+
schemasToSearch = [schemaFilter];
|
|
3969
|
+
} else {
|
|
3970
|
+
schemasToSearch = await connector.getSchemas();
|
|
3971
|
+
}
|
|
3972
|
+
for (const schemaName of schemasToSearch) {
|
|
3973
|
+
if (results.length >= limit) break;
|
|
3974
|
+
try {
|
|
3975
|
+
const procedures = await connector.getStoredProcedures(schemaName);
|
|
3976
|
+
const matched = procedures.filter((proc) => regex.test(proc));
|
|
3977
|
+
for (const procName of matched) {
|
|
3978
|
+
if (results.length >= limit) break;
|
|
3979
|
+
if (detailLevel === "names") {
|
|
3980
|
+
results.push({
|
|
3981
|
+
name: procName,
|
|
3982
|
+
schema: schemaName
|
|
3983
|
+
});
|
|
3984
|
+
} else {
|
|
3985
|
+
try {
|
|
3986
|
+
const details = await connector.getStoredProcedureDetail(procName, schemaName);
|
|
3987
|
+
results.push({
|
|
3988
|
+
name: procName,
|
|
3989
|
+
schema: schemaName,
|
|
3990
|
+
type: details.procedure_type,
|
|
3991
|
+
language: details.language,
|
|
3992
|
+
parameters: detailLevel === "full" ? details.parameter_list : void 0,
|
|
3993
|
+
return_type: details.return_type,
|
|
3994
|
+
definition: detailLevel === "full" ? details.definition : void 0
|
|
3995
|
+
});
|
|
3996
|
+
} catch (error) {
|
|
3997
|
+
results.push({
|
|
3998
|
+
name: procName,
|
|
3999
|
+
schema: schemaName,
|
|
4000
|
+
error: `Unable to fetch details: ${error.message}`
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
} catch (error) {
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
return results;
|
|
4010
|
+
}
|
|
4011
|
+
async function searchIndexes(connector, pattern, schemaFilter, detailLevel, limit) {
|
|
4012
|
+
const regex = likePatternToRegex(pattern);
|
|
4013
|
+
const results = [];
|
|
4014
|
+
let schemasToSearch;
|
|
4015
|
+
if (schemaFilter) {
|
|
4016
|
+
schemasToSearch = [schemaFilter];
|
|
4017
|
+
} else {
|
|
4018
|
+
schemasToSearch = await connector.getSchemas();
|
|
4019
|
+
}
|
|
4020
|
+
for (const schemaName of schemasToSearch) {
|
|
4021
|
+
if (results.length >= limit) break;
|
|
4022
|
+
try {
|
|
4023
|
+
const tables = await connector.getTables(schemaName);
|
|
4024
|
+
for (const tableName of tables) {
|
|
4025
|
+
if (results.length >= limit) break;
|
|
4026
|
+
try {
|
|
4027
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
4028
|
+
const matchedIndexes = indexes.filter((idx) => regex.test(idx.index_name));
|
|
4029
|
+
for (const index of matchedIndexes) {
|
|
4030
|
+
if (results.length >= limit) break;
|
|
4031
|
+
if (detailLevel === "names") {
|
|
4032
|
+
results.push({
|
|
4033
|
+
name: index.index_name,
|
|
4034
|
+
table: tableName,
|
|
4035
|
+
schema: schemaName
|
|
4036
|
+
});
|
|
4037
|
+
} else {
|
|
4038
|
+
results.push({
|
|
4039
|
+
name: index.index_name,
|
|
4040
|
+
table: tableName,
|
|
4041
|
+
schema: schemaName,
|
|
4042
|
+
columns: index.column_names,
|
|
4043
|
+
unique: index.is_unique,
|
|
4044
|
+
primary: index.is_primary
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
} catch (error) {
|
|
4049
|
+
continue;
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
} catch (error) {
|
|
4053
|
+
continue;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return results;
|
|
4057
|
+
}
|
|
4058
|
+
function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
4059
|
+
return async (args, _extra) => {
|
|
4060
|
+
const {
|
|
4061
|
+
object_type,
|
|
4062
|
+
pattern = "%",
|
|
4063
|
+
schema,
|
|
4064
|
+
detail_level = "names",
|
|
4065
|
+
limit = 100
|
|
4066
|
+
} = args;
|
|
4067
|
+
try {
|
|
4068
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
4069
|
+
if (schema) {
|
|
4070
|
+
const schemas = await connector.getSchemas();
|
|
4071
|
+
if (!schemas.includes(schema)) {
|
|
4072
|
+
return createToolErrorResponse(
|
|
4073
|
+
`Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`,
|
|
4074
|
+
"SCHEMA_NOT_FOUND"
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
let results = [];
|
|
4079
|
+
switch (object_type) {
|
|
4080
|
+
case "schema":
|
|
4081
|
+
results = await searchSchemas(connector, pattern, detail_level, limit);
|
|
4082
|
+
break;
|
|
4083
|
+
case "table":
|
|
4084
|
+
results = await searchTables(connector, pattern, schema, detail_level, limit);
|
|
4085
|
+
break;
|
|
4086
|
+
case "column":
|
|
4087
|
+
results = await searchColumns(connector, pattern, schema, detail_level, limit);
|
|
4088
|
+
break;
|
|
4089
|
+
case "procedure":
|
|
4090
|
+
results = await searchProcedures(connector, pattern, schema, detail_level, limit);
|
|
4091
|
+
break;
|
|
4092
|
+
case "index":
|
|
4093
|
+
results = await searchIndexes(connector, pattern, schema, detail_level, limit);
|
|
4094
|
+
break;
|
|
4095
|
+
default:
|
|
4096
|
+
return createToolErrorResponse(
|
|
4097
|
+
`Unsupported object_type: ${object_type}`,
|
|
4098
|
+
"INVALID_OBJECT_TYPE"
|
|
4099
|
+
);
|
|
4100
|
+
}
|
|
4101
|
+
return createToolSuccessResponse({
|
|
4102
|
+
object_type,
|
|
4103
|
+
pattern,
|
|
4104
|
+
schema,
|
|
4105
|
+
detail_level,
|
|
4106
|
+
count: results.length,
|
|
4107
|
+
results,
|
|
4108
|
+
truncated: results.length === limit
|
|
4109
|
+
});
|
|
4110
|
+
} catch (error) {
|
|
3548
4111
|
return createToolErrorResponse(
|
|
3549
|
-
`
|
|
3550
|
-
"
|
|
4112
|
+
`Error searching database objects: ${error.message}`,
|
|
4113
|
+
"SEARCH_ERROR"
|
|
3551
4114
|
);
|
|
3552
4115
|
}
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
// src/utils/tool-metadata.ts
|
|
4120
|
+
import { z as z3 } from "zod";
|
|
4121
|
+
|
|
4122
|
+
// src/utils/normalize-id.ts
|
|
4123
|
+
function normalizeSourceId(id) {
|
|
4124
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
// src/utils/tool-metadata.ts
|
|
4128
|
+
function zodToParameters(schema) {
|
|
4129
|
+
const parameters = [];
|
|
4130
|
+
for (const [key, zodType] of Object.entries(schema)) {
|
|
4131
|
+
const description = zodType.description || "";
|
|
4132
|
+
const required = !(zodType instanceof z3.ZodOptional);
|
|
4133
|
+
let type = "string";
|
|
4134
|
+
if (zodType instanceof z3.ZodString) {
|
|
4135
|
+
type = "string";
|
|
4136
|
+
} else if (zodType instanceof z3.ZodNumber) {
|
|
4137
|
+
type = "number";
|
|
4138
|
+
} else if (zodType instanceof z3.ZodBoolean) {
|
|
4139
|
+
type = "boolean";
|
|
4140
|
+
} else if (zodType instanceof z3.ZodArray) {
|
|
4141
|
+
type = "array";
|
|
4142
|
+
} else if (zodType instanceof z3.ZodObject) {
|
|
4143
|
+
type = "object";
|
|
4144
|
+
}
|
|
4145
|
+
parameters.push({
|
|
4146
|
+
name: key,
|
|
4147
|
+
type,
|
|
4148
|
+
required,
|
|
4149
|
+
description
|
|
4150
|
+
});
|
|
3563
4151
|
}
|
|
4152
|
+
return parameters;
|
|
4153
|
+
}
|
|
4154
|
+
function getToolMetadataForSource(sourceId) {
|
|
4155
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
4156
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
4157
|
+
const executeOptions = ConnectorManager.getCurrentExecuteOptions(sourceId);
|
|
4158
|
+
const dbType = sourceConfig?.type || "database";
|
|
4159
|
+
const toolName = sourceId === "default" ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
4160
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
4161
|
+
const title = isDefault ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
4162
|
+
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
4163
|
+
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
4164
|
+
const description = `Execute SQL queries on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}${readonlyNote}${maxRowsNote}`;
|
|
4165
|
+
const isReadonly = executeOptions.readonly === true;
|
|
4166
|
+
const annotations = {
|
|
4167
|
+
title,
|
|
4168
|
+
readOnlyHint: isReadonly,
|
|
4169
|
+
destructiveHint: !isReadonly,
|
|
4170
|
+
// Can be destructive if not readonly
|
|
4171
|
+
// In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
|
|
4172
|
+
// In write mode, queries are definitely not idempotent
|
|
4173
|
+
idempotentHint: false,
|
|
4174
|
+
// In readonly mode, it's safer to operate on arbitrary tables (just reading)
|
|
4175
|
+
// In write mode, operating on arbitrary tables is more dangerous
|
|
4176
|
+
openWorldHint: isReadonly
|
|
4177
|
+
};
|
|
4178
|
+
return {
|
|
4179
|
+
name: toolName,
|
|
4180
|
+
description,
|
|
4181
|
+
schema: executeSqlSchema,
|
|
4182
|
+
annotations
|
|
4183
|
+
};
|
|
4184
|
+
}
|
|
4185
|
+
function getToolsForSource(sourceId) {
|
|
4186
|
+
const metadata = getToolMetadataForSource(sourceId);
|
|
4187
|
+
const parameters = zodToParameters(metadata.schema);
|
|
4188
|
+
return [
|
|
4189
|
+
{
|
|
4190
|
+
name: metadata.name,
|
|
4191
|
+
description: metadata.description,
|
|
4192
|
+
parameters
|
|
4193
|
+
}
|
|
4194
|
+
];
|
|
3564
4195
|
}
|
|
3565
4196
|
|
|
3566
4197
|
// src/tools/index.ts
|
|
3567
|
-
function registerTools(server
|
|
3568
|
-
const
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
4198
|
+
function registerTools(server) {
|
|
4199
|
+
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
4200
|
+
if (sourceIds.length === 0) {
|
|
4201
|
+
throw new Error("No database sources configured");
|
|
4202
|
+
}
|
|
4203
|
+
for (const sourceId of sourceIds) {
|
|
4204
|
+
const isDefault = sourceIds[0] === sourceId;
|
|
4205
|
+
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
4206
|
+
const dbType = sourceConfig?.type || "database";
|
|
4207
|
+
const executeSqlMetadata = getToolMetadataForSource(sourceId);
|
|
4208
|
+
server.registerTool(
|
|
4209
|
+
executeSqlMetadata.name,
|
|
4210
|
+
{
|
|
4211
|
+
description: executeSqlMetadata.description,
|
|
4212
|
+
inputSchema: executeSqlMetadata.schema,
|
|
4213
|
+
annotations: executeSqlMetadata.annotations
|
|
4214
|
+
},
|
|
4215
|
+
createExecuteSqlToolHandler(sourceId)
|
|
4216
|
+
);
|
|
4217
|
+
const searchToolName = sourceId === "default" ? "search_objects" : `search_objects_${sourceId}`;
|
|
4218
|
+
const searchToolTitle = isDefault ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
4219
|
+
const searchToolDescription = `Search and list database objects (schemas, tables, columns, procedures, indexes) on the '${sourceId}' ${dbType} database${isDefault ? " (default)" : ""}. Supports SQL LIKE patterns (default: '%' for all), filtering, and token-efficient progressive disclosure.`;
|
|
4220
|
+
server.registerTool(
|
|
4221
|
+
searchToolName,
|
|
4222
|
+
{
|
|
4223
|
+
description: searchToolDescription,
|
|
4224
|
+
inputSchema: searchDatabaseObjectsSchema,
|
|
4225
|
+
annotations: {
|
|
4226
|
+
title: searchToolTitle,
|
|
4227
|
+
readOnlyHint: true,
|
|
4228
|
+
destructiveHint: false,
|
|
4229
|
+
idempotentHint: true,
|
|
4230
|
+
// Operation is read-only and idempotent
|
|
4231
|
+
openWorldHint: true
|
|
4232
|
+
}
|
|
4233
|
+
},
|
|
4234
|
+
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
3575
4237
|
}
|
|
3576
4238
|
|
|
3577
4239
|
// src/prompts/sql-generator.ts
|
|
3578
|
-
import { z as
|
|
4240
|
+
import { z as z4 } from "zod";
|
|
3579
4241
|
var sqlGeneratorSchema = {
|
|
3580
|
-
description:
|
|
3581
|
-
schema:
|
|
4242
|
+
description: z4.string().describe("Natural language description of the SQL query to generate"),
|
|
4243
|
+
schema: z4.string().optional().describe("Optional database schema to use")
|
|
3582
4244
|
};
|
|
3583
4245
|
async function sqlGeneratorPromptHandler({
|
|
3584
4246
|
description,
|
|
@@ -3745,10 +4407,10 @@ LIMIT 10;`;
|
|
|
3745
4407
|
}
|
|
3746
4408
|
|
|
3747
4409
|
// src/prompts/db-explainer.ts
|
|
3748
|
-
import { z as
|
|
4410
|
+
import { z as z5 } from "zod";
|
|
3749
4411
|
var dbExplainerSchema = {
|
|
3750
|
-
schema:
|
|
3751
|
-
table:
|
|
4412
|
+
schema: z5.string().optional().describe("Optional database schema to use"),
|
|
4413
|
+
table: z5.string().optional().describe("Optional specific table to explain")
|
|
3752
4414
|
};
|
|
3753
4415
|
async function dbExplainerPromptHandler({
|
|
3754
4416
|
schema,
|
|
@@ -4036,6 +4698,7 @@ function transformSourceConfig(source, isDefault) {
|
|
|
4036
4698
|
}
|
|
4037
4699
|
dataSource.ssh_tunnel = sshTunnel;
|
|
4038
4700
|
}
|
|
4701
|
+
dataSource.tools = getToolsForSource(source.id);
|
|
4039
4702
|
return dataSource;
|
|
4040
4703
|
}
|
|
4041
4704
|
function listSources(req, res) {
|
|
@@ -4079,6 +4742,23 @@ function getSource(req, res) {
|
|
|
4079
4742
|
}
|
|
4080
4743
|
}
|
|
4081
4744
|
|
|
4745
|
+
// src/api/requests.ts
|
|
4746
|
+
function listRequests(req, res) {
|
|
4747
|
+
try {
|
|
4748
|
+
const sourceId = req.query.source_id;
|
|
4749
|
+
const requests = requestStore.getAll(sourceId);
|
|
4750
|
+
res.json({
|
|
4751
|
+
requests,
|
|
4752
|
+
total: requests.length
|
|
4753
|
+
});
|
|
4754
|
+
} catch (error) {
|
|
4755
|
+
console.error("Error listing requests:", error);
|
|
4756
|
+
res.status(500).json({
|
|
4757
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4082
4762
|
// src/server.ts
|
|
4083
4763
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
4084
4764
|
var __dirname2 = path3.dirname(__filename2);
|
|
@@ -4101,12 +4781,10 @@ v${version}${modeText} - Universal Database MCP Server
|
|
|
4101
4781
|
}
|
|
4102
4782
|
async function main() {
|
|
4103
4783
|
try {
|
|
4104
|
-
const idData = resolveId();
|
|
4105
|
-
const id = idData?.id;
|
|
4106
4784
|
const sourceConfigsData = await resolveSourceConfigs();
|
|
4107
4785
|
if (!sourceConfigsData) {
|
|
4108
4786
|
const samples = ConnectorRegistry.getAllSampleDSNs();
|
|
4109
|
-
const sampleFormats = Object.entries(samples).map(([
|
|
4787
|
+
const sampleFormats = Object.entries(samples).map(([id, dsn]) => ` - ${id}: ${dsn}`).join("\n");
|
|
4110
4788
|
console.error(`
|
|
4111
4789
|
ERROR: Database connection configuration is required.
|
|
4112
4790
|
Please provide configuration in one of these ways (in order of priority):
|
|
@@ -4135,16 +4813,13 @@ See documentation for more details on configuring database connections.
|
|
|
4135
4813
|
version: SERVER_VERSION
|
|
4136
4814
|
});
|
|
4137
4815
|
registerResources(server);
|
|
4138
|
-
registerTools(server
|
|
4816
|
+
registerTools(server);
|
|
4139
4817
|
registerPrompts(server);
|
|
4140
4818
|
return server;
|
|
4141
4819
|
};
|
|
4142
4820
|
const connectorManager = new ConnectorManager();
|
|
4143
4821
|
const sources = sourceConfigsData.sources;
|
|
4144
4822
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
4145
|
-
if (idData) {
|
|
4146
|
-
console.error(`ID: ${idData.id} (from ${idData.source})`);
|
|
4147
|
-
}
|
|
4148
4823
|
console.error(`Connecting to ${sources.length} database source(s)...`);
|
|
4149
4824
|
for (const source of sources) {
|
|
4150
4825
|
const dsn = source.dsn || buildDSNFromSource(source);
|
|
@@ -4201,6 +4876,7 @@ See documentation for more details on configuring database connections.
|
|
|
4201
4876
|
});
|
|
4202
4877
|
app.get("/api/sources", listSources);
|
|
4203
4878
|
app.get("/api/sources/:sourceId", getSource);
|
|
4879
|
+
app.get("/api/requests", listRequests);
|
|
4204
4880
|
app.get("/mcp", (req, res) => {
|
|
4205
4881
|
res.status(405).json({
|
|
4206
4882
|
error: "Method Not Allowed",
|