@zz1996/dbhub-dameng 0.1.7 → 1.2.5-dameng.621
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 +126 -137
- package/dbhub.dameng.toml.example +4 -11
- package/dist/{chunk-SQA2ISDE.js → chunk-2JL4KCP5.js} +21 -119
- package/dist/chunk-3RJRH3CN.js +121 -0
- package/dist/chunk-4276LPPU.js +179 -0
- package/dist/chunk-FU2ZJE4E.js +16 -0
- package/dist/chunk-HDPZAIDL.js +155 -0
- package/dist/{chunk-2A2QF3CS.js → chunk-HGU7ILEZ.js} +215 -68
- package/dist/{chunk-IPK7BYBL.js → chunk-JFWX35TB.js} +0 -1
- package/dist/chunk-O3PEWKBB.js +302 -0
- package/dist/dameng-NKF7NZKE.js +250 -0
- package/dist/index.js +461 -251
- package/dist/{mariadb-7F72IRB4.js → mariadb-MMC6WMEG.js} +86 -53
- package/dist/{mysql-A43SL7UM.js → mysql-TESH7KKV.js} +126 -50
- package/dist/{postgres-VFNLBWMF.js → postgres-C24SBQ4I.js} +154 -36
- package/dist/public/assets/index-BJ-1UrcV.css +1 -0
- package/dist/public/assets/index-BdBaFSHd.js +148 -0
- package/dist/public/assets/postgres-BpcazhJg.svg +22 -0
- package/dist/public/assets/sqlserver-ByfFYYpV.svg +11 -0
- package/dist/public/favicon.svg +57 -0
- package/dist/public/index.html +14 -0
- package/dist/public/logo-full-light.svg +58 -0
- package/dist/{registry-XSX2VZTD.js → registry-WVSFVS35.js} +3 -2
- package/dist/{sqlite-IOUAYHGE.js → sqlite-7BNGDMBZ.js} +57 -42
- package/dist/sqlserver-KZPPK67Q.js +1042 -0
- package/package.json +22 -65
- package/dist/chunk-RTB262PR.js +0 -60
- package/dist/chunk-ZNQTMARG.js +0 -127
- package/dist/dameng-5KYI5JBK.js +0 -898
- package/dist/sqlserver-OE4SFH4B.js +0 -587
- package/docs/README.md +0 -14
- package/docs/dameng-connector-plan.md +0 -143
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
|
+
BUILTIN_TOOL_EXPLAIN_SQL,
|
|
5
|
+
BUILTIN_TOOL_HEALTH_CHECK,
|
|
4
6
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
5
7
|
ConnectorManager,
|
|
6
8
|
classifyConnectionError,
|
|
@@ -10,31 +12,41 @@ import {
|
|
|
10
12
|
loadTomlConfig,
|
|
11
13
|
mapArgumentsToArray,
|
|
12
14
|
resolveAllowedHosts,
|
|
15
|
+
resolveAuthTokens,
|
|
13
16
|
resolveHost,
|
|
14
17
|
resolvePort,
|
|
15
18
|
resolveSourceConfigs,
|
|
16
19
|
resolveTomlConfigPath,
|
|
17
20
|
resolveTransport
|
|
18
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-HGU7ILEZ.js";
|
|
19
22
|
import {
|
|
20
23
|
loadConnectors
|
|
21
24
|
} from "./chunk-WVVMH6FJ.js";
|
|
22
25
|
import {
|
|
23
26
|
quoteQualifiedIdentifier
|
|
24
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-JFWX35TB.js";
|
|
25
28
|
import {
|
|
26
|
-
ConnectorRegistry,
|
|
27
29
|
getDatabaseTypeFromDSN,
|
|
28
30
|
getDefaultPortForType,
|
|
29
|
-
parseConnectionInfoFromDSN
|
|
31
|
+
parseConnectionInfoFromDSN
|
|
32
|
+
} from "./chunk-3RJRH3CN.js";
|
|
33
|
+
import {
|
|
34
|
+
allowedKeywords,
|
|
35
|
+
getFirstKeyword,
|
|
36
|
+
hasEscapeHatchFunction,
|
|
37
|
+
isReadOnlySQL,
|
|
38
|
+
sqlServerDynamicSqlPattern
|
|
39
|
+
} from "./chunk-HDPZAIDL.js";
|
|
40
|
+
import {
|
|
41
|
+
ConnectorRegistry,
|
|
30
42
|
splitSQLStatements,
|
|
31
43
|
stripCommentsAndStrings
|
|
32
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-2JL4KCP5.js";
|
|
33
45
|
|
|
34
46
|
// src/server.ts
|
|
35
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
47
|
+
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
|
|
48
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
49
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
38
50
|
import express from "express";
|
|
39
51
|
import http from "http";
|
|
40
52
|
import path from "path";
|
|
@@ -95,98 +107,51 @@ function createToolSuccessResponse(data, meta = {}) {
|
|
|
95
107
|
};
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
// src/utils/
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
|
|
106
|
-
sqlserver: ["select", "with", "explain"],
|
|
107
|
-
dameng: ["select", "with", "explain", "describe", "desc"]
|
|
110
|
+
// src/utils/sql-access-policy.ts
|
|
111
|
+
var CLASS_SEVERITY = {
|
|
112
|
+
read: 0,
|
|
113
|
+
dml: 1,
|
|
114
|
+
ddl: 2,
|
|
115
|
+
admin: 3,
|
|
116
|
+
unknown: 4
|
|
108
117
|
};
|
|
109
|
-
var
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"
|
|
116
|
-
"truncate",
|
|
117
|
-
"merge",
|
|
118
|
-
"grant",
|
|
119
|
-
"revoke",
|
|
120
|
-
"rename"
|
|
121
|
-
];
|
|
122
|
-
var mutatingPattern = new RegExp(
|
|
123
|
-
`\\b(?:${mutatingKeywords.join("|")})\\b`,
|
|
124
|
-
"i"
|
|
125
|
-
);
|
|
126
|
-
var mutatingPatternWithReplace = new RegExp(
|
|
127
|
-
`\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
|
|
128
|
-
"i"
|
|
129
|
-
);
|
|
130
|
-
var mutatingPatterns = {
|
|
131
|
-
postgres: mutatingPattern,
|
|
132
|
-
mysql: mutatingPatternWithReplace,
|
|
133
|
-
mariadb: mutatingPatternWithReplace,
|
|
134
|
-
sqlite: mutatingPatternWithReplace,
|
|
135
|
-
sqlserver: mutatingPattern,
|
|
136
|
-
dameng: mutatingPattern
|
|
137
|
-
};
|
|
138
|
-
var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
|
|
139
|
-
var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
|
|
140
|
-
"table_info",
|
|
141
|
-
"index_info",
|
|
142
|
-
"index_list",
|
|
143
|
-
"foreign_key_list"
|
|
144
|
-
]);
|
|
145
|
-
var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
|
|
146
|
-
var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
|
|
147
|
-
function isReadOnlySQL(sql, connectorType) {
|
|
148
|
-
return checkReadOnly(
|
|
149
|
-
stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(),
|
|
150
|
-
connectorType
|
|
151
|
-
);
|
|
118
|
+
var ddlPattern = /\b(?:create|alter|drop|truncate|rename)\b/i;
|
|
119
|
+
var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|delayed)\s+)?into)\b/i;
|
|
120
|
+
var grantRevokePattern = /\b(?:grant|revoke)\b/i;
|
|
121
|
+
function isAdminStatement(stripped, connectorType) {
|
|
122
|
+
if (grantRevokePattern.test(stripped)) return true;
|
|
123
|
+
if (hasEscapeHatchFunction(stripped, connectorType)) return true;
|
|
124
|
+
return connectorType === "sqlserver" && sqlServerDynamicSqlPattern.test(stripped);
|
|
152
125
|
}
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
if (firstWord === "explain") {
|
|
181
|
-
const m = explainAnalyzePattern.exec(cleanedSQL);
|
|
182
|
-
if (m) {
|
|
183
|
-
const afterExplain = cleanedSQL.slice(m[0].length).trim();
|
|
184
|
-
if (afterExplain && !checkReadOnly(afterExplain, connectorType)) {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
126
|
+
function policyFromReadonly(readonly) {
|
|
127
|
+
const write = readonly === true ? "deny" : "allow";
|
|
128
|
+
return { read: "allow", dml: write, ddl: write, admin: write, unknown: write };
|
|
129
|
+
}
|
|
130
|
+
function isReadOnlyPolicy(policy) {
|
|
131
|
+
return policy.dml === "deny" && policy.ddl === "deny" && policy.admin === "deny" && policy.unknown === "deny";
|
|
132
|
+
}
|
|
133
|
+
function classifyStatement(sql, connectorType) {
|
|
134
|
+
if (isReadOnlySQL(sql, connectorType)) {
|
|
135
|
+
return "read";
|
|
136
|
+
}
|
|
137
|
+
const stripped = stripCommentsAndStrings(sql, connectorType);
|
|
138
|
+
if (isAdminStatement(stripped, connectorType)) return "admin";
|
|
139
|
+
if (ddlPattern.test(stripped)) return "ddl";
|
|
140
|
+
if (dmlPattern.test(stripped)) return "dml";
|
|
141
|
+
return "unknown";
|
|
142
|
+
}
|
|
143
|
+
function classifySQL(sql, connectorType) {
|
|
144
|
+
let strictest = "read";
|
|
145
|
+
for (const statement of splitSQLStatements(sql, connectorType)) {
|
|
146
|
+
const cls = classifyStatement(statement, connectorType);
|
|
147
|
+
if (CLASS_SEVERITY[cls] > CLASS_SEVERITY[strictest]) {
|
|
148
|
+
strictest = cls;
|
|
187
149
|
}
|
|
188
150
|
}
|
|
189
|
-
return
|
|
151
|
+
return strictest;
|
|
152
|
+
}
|
|
153
|
+
function sqlVerdict(policy, sql, connectorType) {
|
|
154
|
+
return policy[classifySQL(sql, connectorType)];
|
|
190
155
|
}
|
|
191
156
|
|
|
192
157
|
// src/requests/store.ts
|
|
@@ -240,18 +205,41 @@ var RequestStore = class {
|
|
|
240
205
|
var requestStore = new RequestStore();
|
|
241
206
|
|
|
242
207
|
// src/utils/client-identifier.ts
|
|
243
|
-
function getClientIdentifier(
|
|
244
|
-
const userAgent =
|
|
208
|
+
function getClientIdentifier(ctx) {
|
|
209
|
+
const userAgent = ctx?.http?.req?.headers.get("user-agent");
|
|
245
210
|
if (userAgent) {
|
|
246
211
|
return userAgent;
|
|
247
212
|
}
|
|
248
213
|
return "stdio";
|
|
249
214
|
}
|
|
250
215
|
|
|
216
|
+
// src/utils/normalize-id.ts
|
|
217
|
+
function normalizeSourceId(id) {
|
|
218
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
219
|
+
}
|
|
220
|
+
|
|
251
221
|
// src/utils/tool-handler-helpers.ts
|
|
252
222
|
function getEffectiveSourceId(sourceId) {
|
|
253
223
|
return sourceId || "default";
|
|
254
224
|
}
|
|
225
|
+
function toStatementsPayload(resultSets) {
|
|
226
|
+
return resultSets.map((set) => ({
|
|
227
|
+
sql: set.sql,
|
|
228
|
+
rows: set.rows,
|
|
229
|
+
count: set.rowCount,
|
|
230
|
+
// Only present when a max_rows cap actually cut off rows, so consumers
|
|
231
|
+
// can tell a capped result from a table with exactly max_rows rows
|
|
232
|
+
// (and complete results don't pay the extra tokens).
|
|
233
|
+
...set.truncated ? { truncated: true } : {}
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
function resolveTrackedToolName(sourceId, baseName) {
|
|
237
|
+
const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
|
|
238
|
+
if (isSingleSource) {
|
|
239
|
+
return baseName;
|
|
240
|
+
}
|
|
241
|
+
return `${baseName}_${normalizeSourceId(getEffectiveSourceId(sourceId))}`;
|
|
242
|
+
}
|
|
255
243
|
function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
|
|
256
244
|
return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
|
|
257
245
|
}
|
|
@@ -287,10 +275,7 @@ function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
|
|
|
287
275
|
var executeSqlSchema = {
|
|
288
276
|
sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
|
|
289
277
|
};
|
|
290
|
-
|
|
291
|
-
const statements = splitSQLStatements(sql, connectorType);
|
|
292
|
-
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
293
|
-
}
|
|
278
|
+
var executeSqlInputSchema = z.object(executeSqlSchema);
|
|
294
279
|
function createExecuteSqlToolHandler(sourceId) {
|
|
295
280
|
return async (args, extra) => {
|
|
296
281
|
const { sql } = args;
|
|
@@ -305,20 +290,19 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
305
290
|
const actualSourceId = connector.getId();
|
|
306
291
|
const registry = getToolRegistry();
|
|
307
292
|
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
293
|
+
const policy = policyFromReadonly(toolConfig?.readonly);
|
|
294
|
+
if (sqlVerdict(policy, sql, connector.id) === "deny") {
|
|
310
295
|
errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
|
|
311
296
|
success = false;
|
|
312
297
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
313
298
|
}
|
|
314
299
|
const executeOptions = {
|
|
315
|
-
readonly:
|
|
300
|
+
readonly: isReadOnlyPolicy(policy),
|
|
316
301
|
maxRows: toolConfig?.max_rows
|
|
317
302
|
};
|
|
318
303
|
result = await connector.executeSQL(sql, executeOptions);
|
|
319
304
|
const responseData = {
|
|
320
|
-
|
|
321
|
-
count: result.rowCount,
|
|
305
|
+
statements: toStatementsPayload(result.resultSets),
|
|
322
306
|
source_id: effectiveSourceId,
|
|
323
307
|
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
324
308
|
};
|
|
@@ -355,6 +339,7 @@ var searchDatabaseObjectsSchema = {
|
|
|
355
339
|
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
|
|
356
340
|
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
|
|
357
341
|
};
|
|
342
|
+
var searchDatabaseObjectsInputSchema = z2.object(searchDatabaseObjectsSchema);
|
|
358
343
|
function likePatternToRegex(pattern) {
|
|
359
344
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
360
345
|
return new RegExp(`^${escaped}$`, "i");
|
|
@@ -375,9 +360,10 @@ async function getTableRowCount(connector, tableName, schemaName) {
|
|
|
375
360
|
}
|
|
376
361
|
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
377
362
|
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
378
|
-
const result = await connector.executeSQL(countQuery, { maxRows: 1 });
|
|
379
|
-
|
|
380
|
-
|
|
363
|
+
const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
|
|
364
|
+
const rows = result.resultSets[0]?.rows;
|
|
365
|
+
if (rows && rows.length > 0) {
|
|
366
|
+
return Number(rows[0].count || rows[0].COUNT || 0);
|
|
381
367
|
}
|
|
382
368
|
} catch (error) {
|
|
383
369
|
return null;
|
|
@@ -431,25 +417,23 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
431
417
|
for (const schemaName of schemasToSearch) {
|
|
432
418
|
if (results.length >= limit) break;
|
|
433
419
|
try {
|
|
434
|
-
const
|
|
435
|
-
const matched =
|
|
436
|
-
for (const
|
|
420
|
+
const tables = await connector.getTables(schemaName);
|
|
421
|
+
const matched = tables.filter((table) => regex.test(table));
|
|
422
|
+
for (const tableName of matched) {
|
|
437
423
|
if (results.length >= limit) break;
|
|
438
|
-
const tableName = tableMatch.name;
|
|
439
|
-
const matchedSchema = tableMatch.schema;
|
|
440
424
|
if (detailLevel === "names") {
|
|
441
425
|
results.push({
|
|
442
426
|
name: tableName,
|
|
443
|
-
schema:
|
|
427
|
+
schema: schemaName
|
|
444
428
|
});
|
|
445
429
|
} else if (detailLevel === "summary") {
|
|
446
430
|
try {
|
|
447
|
-
const columns = await connector.getTableSchema(tableName,
|
|
448
|
-
const rowCount = await getTableRowCount(connector, tableName,
|
|
449
|
-
const comment = await getTableComment(connector, tableName,
|
|
431
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
432
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
433
|
+
const comment = await getTableComment(connector, tableName, schemaName);
|
|
450
434
|
results.push({
|
|
451
435
|
name: tableName,
|
|
452
|
-
schema:
|
|
436
|
+
schema: schemaName,
|
|
453
437
|
column_count: columns.length,
|
|
454
438
|
row_count: rowCount,
|
|
455
439
|
...comment ? { comment } : {}
|
|
@@ -457,20 +441,20 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
457
441
|
} catch (error) {
|
|
458
442
|
results.push({
|
|
459
443
|
name: tableName,
|
|
460
|
-
schema:
|
|
444
|
+
schema: schemaName,
|
|
461
445
|
column_count: null,
|
|
462
446
|
row_count: null
|
|
463
447
|
});
|
|
464
448
|
}
|
|
465
449
|
} else {
|
|
466
450
|
try {
|
|
467
|
-
const columns = await connector.getTableSchema(tableName,
|
|
468
|
-
const indexes = await connector.getTableIndexes(tableName,
|
|
469
|
-
const rowCount = await getTableRowCount(connector, tableName,
|
|
470
|
-
const comment = await getTableComment(connector, tableName,
|
|
451
|
+
const columns = await connector.getTableSchema(tableName, schemaName);
|
|
452
|
+
const indexes = await connector.getTableIndexes(tableName, schemaName);
|
|
453
|
+
const rowCount = await getTableRowCount(connector, tableName, schemaName);
|
|
454
|
+
const comment = await getTableComment(connector, tableName, schemaName);
|
|
471
455
|
results.push({
|
|
472
456
|
name: tableName,
|
|
473
|
-
schema:
|
|
457
|
+
schema: schemaName,
|
|
474
458
|
column_count: columns.length,
|
|
475
459
|
row_count: rowCount,
|
|
476
460
|
...comment ? { comment } : {},
|
|
@@ -491,7 +475,7 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
491
475
|
} catch (error) {
|
|
492
476
|
results.push({
|
|
493
477
|
name: tableName,
|
|
494
|
-
schema:
|
|
478
|
+
schema: schemaName,
|
|
495
479
|
error: `Unable to fetch full details: ${error.message}`
|
|
496
480
|
});
|
|
497
481
|
}
|
|
@@ -510,7 +494,7 @@ async function searchViews(connector, pattern, schemaFilter, detailLevel, limit)
|
|
|
510
494
|
if (schemaFilter) {
|
|
511
495
|
schemasToSearch = [schemaFilter];
|
|
512
496
|
} else {
|
|
513
|
-
schemasToSearch = await connector
|
|
497
|
+
schemasToSearch = await resolveDefaultSchemas(connector);
|
|
514
498
|
}
|
|
515
499
|
for (const schemaName of schemasToSearch) {
|
|
516
500
|
if (results.length >= limit) break;
|
|
@@ -586,36 +570,6 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
|
|
|
586
570
|
for (const schemaName of schemasToSearch) {
|
|
587
571
|
if (results.length >= limit) break;
|
|
588
572
|
try {
|
|
589
|
-
const searchableConnector = connector;
|
|
590
|
-
if (searchableConnector.searchColumns) {
|
|
591
|
-
const matchedColumns = await searchableConnector.searchColumns(
|
|
592
|
-
pattern,
|
|
593
|
-
schemaName,
|
|
594
|
-
tableFilter,
|
|
595
|
-
limit - results.length
|
|
596
|
-
);
|
|
597
|
-
for (const column of matchedColumns) {
|
|
598
|
-
if (results.length >= limit) break;
|
|
599
|
-
if (detailLevel === "names") {
|
|
600
|
-
results.push({
|
|
601
|
-
name: column.name,
|
|
602
|
-
table: column.table,
|
|
603
|
-
schema: column.schema
|
|
604
|
-
});
|
|
605
|
-
} else {
|
|
606
|
-
results.push({
|
|
607
|
-
name: column.name,
|
|
608
|
-
table: column.table,
|
|
609
|
-
schema: column.schema,
|
|
610
|
-
type: column.type,
|
|
611
|
-
nullable: column.nullable,
|
|
612
|
-
default: column.default,
|
|
613
|
-
...column.description ? { description: column.description } : {}
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
573
|
let tablesToSearch;
|
|
620
574
|
if (tableFilter) {
|
|
621
575
|
tablesToSearch = [tableFilter];
|
|
@@ -791,10 +745,10 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
791
745
|
}
|
|
792
746
|
}
|
|
793
747
|
if (schema) {
|
|
794
|
-
const
|
|
795
|
-
if (!
|
|
748
|
+
const schemas = await connector.getSchemas();
|
|
749
|
+
if (!schemas.includes(schema)) {
|
|
796
750
|
success = false;
|
|
797
|
-
errorMessage = `Schema '${schema}' does not exist`;
|
|
751
|
+
errorMessage = `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`;
|
|
798
752
|
return createToolErrorResponse(errorMessage, "SCHEMA_NOT_FOUND");
|
|
799
753
|
}
|
|
800
754
|
}
|
|
@@ -861,15 +815,151 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
861
815
|
};
|
|
862
816
|
}
|
|
863
817
|
|
|
864
|
-
// src/
|
|
818
|
+
// src/tools/explain-sql.ts
|
|
865
819
|
import { z as z3 } from "zod";
|
|
820
|
+
var explainSqlSchema = {
|
|
821
|
+
sql: z3.string().describe("Single SQL statement to explain (no trailing semicolon-separated statements)")
|
|
822
|
+
};
|
|
823
|
+
var explainSqlInputSchema = z3.object(explainSqlSchema);
|
|
824
|
+
function buildExplainStatement(connectorType, sql) {
|
|
825
|
+
if (connectorType === "sqlite") {
|
|
826
|
+
return `EXPLAIN QUERY PLAN ${sql}`;
|
|
827
|
+
}
|
|
828
|
+
return `EXPLAIN ${sql}`;
|
|
829
|
+
}
|
|
830
|
+
function validateExplainInput(sql, connectorType) {
|
|
831
|
+
const statements = splitSQLStatements(sql, connectorType);
|
|
832
|
+
if (statements.length !== 1) {
|
|
833
|
+
return "explain_sql only supports a single SQL statement";
|
|
834
|
+
}
|
|
835
|
+
const firstWord = getFirstKeyword(statements[0], connectorType);
|
|
836
|
+
if (firstWord === "explain") {
|
|
837
|
+
return "explain_sql input must not itself start with EXPLAIN";
|
|
838
|
+
}
|
|
839
|
+
if (firstWord === "analyze") {
|
|
840
|
+
return "explain_sql does not support ANALYZE (it must never execute the statement)";
|
|
841
|
+
}
|
|
842
|
+
const cleaned = stripCommentsAndStrings(statements[0], connectorType).trim();
|
|
843
|
+
const leadingOptions = cleaned.match(/^\(([\s\S]*?)\)/)?.[1];
|
|
844
|
+
if (leadingOptions && /\banalyze\b/i.test(leadingOptions)) {
|
|
845
|
+
return "explain_sql does not support ANALYZE (it must never execute the statement)";
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
function createExplainSqlToolHandler(sourceId) {
|
|
850
|
+
return async (args, extra) => {
|
|
851
|
+
const { sql } = args;
|
|
852
|
+
const startTime = Date.now();
|
|
853
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
854
|
+
let success = true;
|
|
855
|
+
let errorMessage;
|
|
856
|
+
let result;
|
|
857
|
+
try {
|
|
858
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
859
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
860
|
+
const validationError = validateExplainInput(sql, connector.id);
|
|
861
|
+
if (validationError) {
|
|
862
|
+
success = false;
|
|
863
|
+
errorMessage = validationError;
|
|
864
|
+
return createToolErrorResponse(errorMessage, "INVALID_INPUT");
|
|
865
|
+
}
|
|
866
|
+
const explainStatement = buildExplainStatement(connector.id, sql);
|
|
867
|
+
result = await connector.executeSQL(explainStatement, { readonly: true });
|
|
868
|
+
const responseData = {
|
|
869
|
+
rows: result.resultSets[0].rows,
|
|
870
|
+
count: result.resultSets[0].rowCount,
|
|
871
|
+
source_id: effectiveSourceId,
|
|
872
|
+
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
873
|
+
};
|
|
874
|
+
return createToolSuccessResponse(responseData);
|
|
875
|
+
} catch (error) {
|
|
876
|
+
success = false;
|
|
877
|
+
errorMessage = error.message;
|
|
878
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
879
|
+
if (classified) return classified;
|
|
880
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
881
|
+
} finally {
|
|
882
|
+
trackToolRequest(
|
|
883
|
+
{
|
|
884
|
+
sourceId: effectiveSourceId,
|
|
885
|
+
toolName: resolveTrackedToolName(sourceId, "explain_sql"),
|
|
886
|
+
sql
|
|
887
|
+
},
|
|
888
|
+
startTime,
|
|
889
|
+
extra,
|
|
890
|
+
success,
|
|
891
|
+
errorMessage
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
866
896
|
|
|
867
|
-
// src/
|
|
868
|
-
|
|
869
|
-
|
|
897
|
+
// src/tools/health-check.ts
|
|
898
|
+
import { z as z4 } from "zod";
|
|
899
|
+
var healthCheckSchema = {};
|
|
900
|
+
var healthCheckInputSchema = z4.object(healthCheckSchema);
|
|
901
|
+
function createHealthCheckToolHandler(sourceId) {
|
|
902
|
+
return async (_args, extra) => {
|
|
903
|
+
const startTime = Date.now();
|
|
904
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
905
|
+
let success = true;
|
|
906
|
+
let errorMessage;
|
|
907
|
+
try {
|
|
908
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
909
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
910
|
+
if (!connector.getHealthCheck) {
|
|
911
|
+
success = false;
|
|
912
|
+
errorMessage = `health_check is not supported for '${connector.id}' sources yet`;
|
|
913
|
+
return createToolErrorResponse(errorMessage, "UNSUPPORTED");
|
|
914
|
+
}
|
|
915
|
+
const health = await connector.getHealthCheck();
|
|
916
|
+
return createToolSuccessResponse({
|
|
917
|
+
source_id: effectiveSourceId,
|
|
918
|
+
...health.connections ? {
|
|
919
|
+
connections: {
|
|
920
|
+
total: health.connections.total,
|
|
921
|
+
active: health.connections.active,
|
|
922
|
+
idle: health.connections.idle,
|
|
923
|
+
idle_in_transaction: health.connections.idleInTransaction,
|
|
924
|
+
...health.connections.idleInTransactionAborted !== void 0 ? { idle_in_transaction_aborted: health.connections.idleInTransactionAborted } : {},
|
|
925
|
+
max_connections: health.connections.maxConnections,
|
|
926
|
+
longest_idle_in_transaction_seconds: health.connections.longestIdleInTransactionSeconds,
|
|
927
|
+
longest_active_query_seconds: health.connections.longestActiveQuerySeconds
|
|
928
|
+
}
|
|
929
|
+
} : {},
|
|
930
|
+
...health.bufferCache ? {
|
|
931
|
+
buffer_cache: {
|
|
932
|
+
hit_ratio_pct: health.bufferCache.hitRatioPct,
|
|
933
|
+
blocks_hit: health.bufferCache.blocksHit,
|
|
934
|
+
blocks_read: health.bufferCache.blocksRead
|
|
935
|
+
}
|
|
936
|
+
} : {},
|
|
937
|
+
...health.notes && health.notes.length > 0 ? { notes: health.notes } : {}
|
|
938
|
+
});
|
|
939
|
+
} catch (error) {
|
|
940
|
+
success = false;
|
|
941
|
+
errorMessage = error.message;
|
|
942
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
943
|
+
if (classified) return classified;
|
|
944
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
945
|
+
} finally {
|
|
946
|
+
trackToolRequest(
|
|
947
|
+
{
|
|
948
|
+
sourceId: effectiveSourceId,
|
|
949
|
+
toolName: effectiveSourceId === "default" ? "health_check" : `health_check_${effectiveSourceId}`,
|
|
950
|
+
sql: ""
|
|
951
|
+
},
|
|
952
|
+
startTime,
|
|
953
|
+
extra,
|
|
954
|
+
success,
|
|
955
|
+
errorMessage
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
};
|
|
870
959
|
}
|
|
871
960
|
|
|
872
961
|
// src/utils/tool-metadata.ts
|
|
962
|
+
import { z as z5 } from "zod";
|
|
873
963
|
function buildSourceDescriptionPrefix(description) {
|
|
874
964
|
const trimmed = description?.trim() ?? "";
|
|
875
965
|
if (!trimmed) return "";
|
|
@@ -879,17 +969,17 @@ function zodToParameters(schema) {
|
|
|
879
969
|
const parameters = [];
|
|
880
970
|
for (const [key, zodType] of Object.entries(schema)) {
|
|
881
971
|
const description = zodType.description || "";
|
|
882
|
-
const required = !(zodType instanceof
|
|
972
|
+
const required = !(zodType instanceof z5.ZodOptional);
|
|
883
973
|
let type = "string";
|
|
884
|
-
if (zodType instanceof
|
|
974
|
+
if (zodType instanceof z5.ZodString) {
|
|
885
975
|
type = "string";
|
|
886
|
-
} else if (zodType instanceof
|
|
976
|
+
} else if (zodType instanceof z5.ZodNumber) {
|
|
887
977
|
type = "number";
|
|
888
|
-
} else if (zodType instanceof
|
|
978
|
+
} else if (zodType instanceof z5.ZodBoolean) {
|
|
889
979
|
type = "boolean";
|
|
890
|
-
} else if (zodType instanceof
|
|
980
|
+
} else if (zodType instanceof z5.ZodArray) {
|
|
891
981
|
type = "array";
|
|
892
|
-
} else if (zodType instanceof
|
|
982
|
+
} else if (zodType instanceof z5.ZodObject) {
|
|
893
983
|
type = "object";
|
|
894
984
|
}
|
|
895
985
|
parameters.push({
|
|
@@ -901,24 +991,32 @@ function zodToParameters(schema) {
|
|
|
901
991
|
}
|
|
902
992
|
return parameters;
|
|
903
993
|
}
|
|
904
|
-
function
|
|
994
|
+
function resolveBuiltinToolNaming(sourceId, baseName, titleLabel) {
|
|
905
995
|
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
906
996
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
907
997
|
const dbType = sourceConfig.type;
|
|
908
998
|
const isSingleSource = sourceIds.length === 1;
|
|
999
|
+
const name = isSingleSource ? baseName : `${baseName}_${normalizeSourceId(sourceId)}`;
|
|
1000
|
+
const title = isSingleSource ? `${titleLabel} (${dbType})` : `${titleLabel} on ${sourceId} (${dbType})`;
|
|
1001
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
1002
|
+
return { dbType, isSingleSource, name, title, userDescPrefix };
|
|
1003
|
+
}
|
|
1004
|
+
function getExecuteSqlMetadata(sourceId) {
|
|
1005
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1006
|
+
sourceId,
|
|
1007
|
+
"execute_sql",
|
|
1008
|
+
"Execute SQL"
|
|
1009
|
+
);
|
|
909
1010
|
const registry = getToolRegistry();
|
|
910
1011
|
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
|
|
911
1012
|
const executeOptions = {
|
|
912
1013
|
readonly: toolConfig?.readonly,
|
|
913
1014
|
maxRows: toolConfig?.max_rows
|
|
914
1015
|
};
|
|
915
|
-
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
916
|
-
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
917
|
-
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
918
1016
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
919
|
-
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
1017
|
+
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows; capped results carry "truncated": true)` : "";
|
|
920
1018
|
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
921
|
-
const isReadonly = executeOptions.readonly
|
|
1019
|
+
const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
|
|
922
1020
|
const annotations = {
|
|
923
1021
|
title,
|
|
924
1022
|
readOnlyHint: isReadonly,
|
|
@@ -931,25 +1029,62 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
931
1029
|
openWorldHint: false
|
|
932
1030
|
};
|
|
933
1031
|
return {
|
|
934
|
-
name
|
|
1032
|
+
name,
|
|
935
1033
|
description,
|
|
936
1034
|
schema: executeSqlSchema,
|
|
937
1035
|
annotations
|
|
938
1036
|
};
|
|
939
1037
|
}
|
|
940
1038
|
function getSearchObjectsMetadata(sourceId) {
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
947
|
-
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
1039
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1040
|
+
sourceId,
|
|
1041
|
+
"search_objects",
|
|
1042
|
+
"Search Database Objects"
|
|
1043
|
+
);
|
|
948
1044
|
const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
|
|
1045
|
+
return { name, description, title };
|
|
1046
|
+
}
|
|
1047
|
+
function getExplainSqlMetadata(sourceId) {
|
|
1048
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1049
|
+
sourceId,
|
|
1050
|
+
"explain_sql",
|
|
1051
|
+
"Explain Query Plan"
|
|
1052
|
+
);
|
|
1053
|
+
const description = isSingleSource ? `${userDescPrefix}Show the execution plan for a SQL statement on the ${dbType} database without running it (always read-only).` : `${userDescPrefix}Show the execution plan for a SQL statement on the '${sourceId}' ${dbType} database without running it (always read-only).`;
|
|
1054
|
+
const annotations = {
|
|
1055
|
+
title,
|
|
1056
|
+
readOnlyHint: true,
|
|
1057
|
+
destructiveHint: false,
|
|
1058
|
+
idempotentHint: true,
|
|
1059
|
+
openWorldHint: false
|
|
1060
|
+
};
|
|
949
1061
|
return {
|
|
950
|
-
name
|
|
1062
|
+
name,
|
|
951
1063
|
description,
|
|
952
|
-
|
|
1064
|
+
schema: explainSqlSchema,
|
|
1065
|
+
annotations
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function getHealthCheckMetadata(sourceId) {
|
|
1069
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1070
|
+
sourceId,
|
|
1071
|
+
"health_check",
|
|
1072
|
+
"Health Check"
|
|
1073
|
+
);
|
|
1074
|
+
const description = isSingleSource ? `${userDescPrefix}Report connection pool and buffer cache health metrics for the ${dbType} database (read-only).` : `${userDescPrefix}Report connection pool and buffer cache health metrics for the '${sourceId}' ${dbType} database (read-only).`;
|
|
1075
|
+
const annotations = {
|
|
1076
|
+
title,
|
|
1077
|
+
readOnlyHint: true,
|
|
1078
|
+
destructiveHint: false,
|
|
1079
|
+
idempotentHint: false,
|
|
1080
|
+
// Metrics change from call to call, unlike explain_sql's plan output
|
|
1081
|
+
openWorldHint: false
|
|
1082
|
+
};
|
|
1083
|
+
return {
|
|
1084
|
+
name,
|
|
1085
|
+
description,
|
|
1086
|
+
schema: healthCheckSchema,
|
|
1087
|
+
annotations
|
|
953
1088
|
};
|
|
954
1089
|
}
|
|
955
1090
|
function customParamsToToolParams(params) {
|
|
@@ -986,13 +1121,13 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
986
1121
|
name: "object_type",
|
|
987
1122
|
type: "string",
|
|
988
1123
|
required: true,
|
|
989
|
-
description: "Object type to search
|
|
1124
|
+
description: "Object type to search"
|
|
990
1125
|
},
|
|
991
1126
|
{
|
|
992
1127
|
name: "pattern",
|
|
993
1128
|
type: "string",
|
|
994
1129
|
required: false,
|
|
995
|
-
description: "LIKE pattern (% = any chars, _ = one char)
|
|
1130
|
+
description: "LIKE pattern (% = any chars, _ = one char)"
|
|
996
1131
|
},
|
|
997
1132
|
{
|
|
998
1133
|
name: "schema",
|
|
@@ -1016,13 +1151,23 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
1016
1151
|
name: "limit",
|
|
1017
1152
|
type: "integer",
|
|
1018
1153
|
required: false,
|
|
1019
|
-
description: "Max results
|
|
1154
|
+
description: "Max results"
|
|
1020
1155
|
}
|
|
1021
1156
|
],
|
|
1022
1157
|
readonly: true
|
|
1023
1158
|
// search_objects is always readonly
|
|
1024
1159
|
};
|
|
1025
1160
|
}
|
|
1161
|
+
function buildExplainSqlTool(sourceId) {
|
|
1162
|
+
const explainMetadata = getExplainSqlMetadata(sourceId);
|
|
1163
|
+
return {
|
|
1164
|
+
name: explainMetadata.name,
|
|
1165
|
+
description: explainMetadata.description,
|
|
1166
|
+
parameters: zodToParameters(explainMetadata.schema),
|
|
1167
|
+
readonly: true
|
|
1168
|
+
// explain_sql is always readonly
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1026
1171
|
function buildCustomTool(toolConfig) {
|
|
1027
1172
|
return {
|
|
1028
1173
|
name: toolConfig.name,
|
|
@@ -1041,6 +1186,8 @@ function getToolsForSource(sourceId) {
|
|
|
1041
1186
|
return buildExecuteSqlTool(sourceId, toolConfig);
|
|
1042
1187
|
} else if (toolConfig.name === "search_objects") {
|
|
1043
1188
|
return buildSearchObjectsTool(sourceId);
|
|
1189
|
+
} else if (toolConfig.name === "explain_sql") {
|
|
1190
|
+
return buildExplainSqlTool(sourceId);
|
|
1044
1191
|
} else {
|
|
1045
1192
|
return buildCustomTool(toolConfig);
|
|
1046
1193
|
}
|
|
@@ -1048,7 +1195,7 @@ function getToolsForSource(sourceId) {
|
|
|
1048
1195
|
}
|
|
1049
1196
|
|
|
1050
1197
|
// src/tools/custom-tool-handler.ts
|
|
1051
|
-
import { z as
|
|
1198
|
+
import { z as z6 } from "zod";
|
|
1052
1199
|
function buildZodSchemaFromParameters(parameters) {
|
|
1053
1200
|
if (!parameters || parameters.length === 0) {
|
|
1054
1201
|
return {};
|
|
@@ -1058,26 +1205,26 @@ function buildZodSchemaFromParameters(parameters) {
|
|
|
1058
1205
|
let fieldSchema;
|
|
1059
1206
|
switch (param.type) {
|
|
1060
1207
|
case "string":
|
|
1061
|
-
fieldSchema =
|
|
1208
|
+
fieldSchema = z6.string().describe(param.description);
|
|
1062
1209
|
break;
|
|
1063
1210
|
case "integer":
|
|
1064
|
-
fieldSchema =
|
|
1211
|
+
fieldSchema = z6.number().int().describe(param.description);
|
|
1065
1212
|
break;
|
|
1066
1213
|
case "float":
|
|
1067
|
-
fieldSchema =
|
|
1214
|
+
fieldSchema = z6.number().describe(param.description);
|
|
1068
1215
|
break;
|
|
1069
1216
|
case "boolean":
|
|
1070
|
-
fieldSchema =
|
|
1217
|
+
fieldSchema = z6.boolean().describe(param.description);
|
|
1071
1218
|
break;
|
|
1072
1219
|
case "array":
|
|
1073
|
-
fieldSchema =
|
|
1220
|
+
fieldSchema = z6.array(z6.unknown()).describe(param.description);
|
|
1074
1221
|
break;
|
|
1075
1222
|
default:
|
|
1076
1223
|
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
1077
1224
|
}
|
|
1078
1225
|
if (param.allowed_values && param.allowed_values.length > 0) {
|
|
1079
1226
|
if (param.type === "string") {
|
|
1080
|
-
fieldSchema =
|
|
1227
|
+
fieldSchema = z6.enum(param.allowed_values).describe(param.description);
|
|
1081
1228
|
} else {
|
|
1082
1229
|
fieldSchema = fieldSchema.refine(
|
|
1083
1230
|
(val) => param.allowed_values.includes(val),
|
|
@@ -1094,9 +1241,17 @@ function buildZodSchemaFromParameters(parameters) {
|
|
|
1094
1241
|
}
|
|
1095
1242
|
return schemaShape;
|
|
1096
1243
|
}
|
|
1244
|
+
var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
|
|
1245
|
+
function getCustomToolInputSchema(toolConfig) {
|
|
1246
|
+
let schema = customToolInputSchemas.get(toolConfig);
|
|
1247
|
+
if (!schema) {
|
|
1248
|
+
schema = z6.object(buildZodSchemaFromParameters(toolConfig.parameters));
|
|
1249
|
+
customToolInputSchemas.set(toolConfig, schema);
|
|
1250
|
+
}
|
|
1251
|
+
return schema;
|
|
1252
|
+
}
|
|
1097
1253
|
function createCustomToolHandler(toolConfig) {
|
|
1098
|
-
const
|
|
1099
|
-
const zodSchema = z4.object(zodSchemaShape);
|
|
1254
|
+
const zodSchema = getCustomToolInputSchema(toolConfig);
|
|
1100
1255
|
return async (args, extra) => {
|
|
1101
1256
|
const startTime = Date.now();
|
|
1102
1257
|
let success = true;
|
|
@@ -1106,12 +1261,12 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1106
1261
|
const validatedArgs = zodSchema.parse(args);
|
|
1107
1262
|
await ConnectorManager.ensureConnected(toolConfig.source);
|
|
1108
1263
|
const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
|
|
1264
|
+
const policy = policyFromReadonly(toolConfig.readonly);
|
|
1109
1265
|
const executeOptions = {
|
|
1110
|
-
readonly:
|
|
1266
|
+
readonly: isReadOnlyPolicy(policy),
|
|
1111
1267
|
maxRows: toolConfig.max_rows
|
|
1112
1268
|
};
|
|
1113
|
-
|
|
1114
|
-
if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
|
|
1269
|
+
if (sqlVerdict(policy, toolConfig.statement, connector.id) === "deny") {
|
|
1115
1270
|
errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
|
|
1116
1271
|
success = false;
|
|
1117
1272
|
return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
|
|
@@ -1126,8 +1281,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1126
1281
|
paramValues
|
|
1127
1282
|
);
|
|
1128
1283
|
const responseData = {
|
|
1129
|
-
|
|
1130
|
-
count: result.rowCount,
|
|
1284
|
+
statements: toStatementsPayload(result.resultSets),
|
|
1131
1285
|
source_id: toolConfig.source
|
|
1132
1286
|
};
|
|
1133
1287
|
return createToolSuccessResponse(responseData);
|
|
@@ -1136,7 +1290,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1136
1290
|
errorMessage = error.message;
|
|
1137
1291
|
const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
|
|
1138
1292
|
if (classified) return classified;
|
|
1139
|
-
if (error instanceof
|
|
1293
|
+
if (error instanceof z6.ZodError) {
|
|
1140
1294
|
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1141
1295
|
errorMessage = `Parameter validation failed: ${issues}`;
|
|
1142
1296
|
} else {
|
|
@@ -1176,6 +1330,10 @@ function registerTools(server) {
|
|
|
1176
1330
|
registerExecuteSqlTool(server, sourceId);
|
|
1177
1331
|
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
1178
1332
|
registerSearchObjectsTool(server, sourceId);
|
|
1333
|
+
} else if (toolConfig.name === BUILTIN_TOOL_EXPLAIN_SQL) {
|
|
1334
|
+
registerExplainSqlTool(server, sourceId);
|
|
1335
|
+
} else if (toolConfig.name === BUILTIN_TOOL_HEALTH_CHECK) {
|
|
1336
|
+
registerHealthCheckTool(server, sourceId);
|
|
1179
1337
|
} else {
|
|
1180
1338
|
registerCustomTool(server, sourceId, toolConfig);
|
|
1181
1339
|
}
|
|
@@ -1188,7 +1346,7 @@ function registerExecuteSqlTool(server, sourceId) {
|
|
|
1188
1346
|
metadata.name,
|
|
1189
1347
|
{
|
|
1190
1348
|
description: metadata.description,
|
|
1191
|
-
inputSchema:
|
|
1349
|
+
inputSchema: executeSqlInputSchema,
|
|
1192
1350
|
annotations: metadata.annotations
|
|
1193
1351
|
},
|
|
1194
1352
|
createExecuteSqlToolHandler(sourceId)
|
|
@@ -1200,7 +1358,7 @@ function registerSearchObjectsTool(server, sourceId) {
|
|
|
1200
1358
|
metadata.name,
|
|
1201
1359
|
{
|
|
1202
1360
|
description: metadata.description,
|
|
1203
|
-
inputSchema:
|
|
1361
|
+
inputSchema: searchDatabaseObjectsInputSchema,
|
|
1204
1362
|
annotations: {
|
|
1205
1363
|
title: metadata.title,
|
|
1206
1364
|
readOnlyHint: true,
|
|
@@ -1212,16 +1370,39 @@ function registerSearchObjectsTool(server, sourceId) {
|
|
|
1212
1370
|
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
1213
1371
|
);
|
|
1214
1372
|
}
|
|
1373
|
+
function registerExplainSqlTool(server, sourceId) {
|
|
1374
|
+
const metadata = getExplainSqlMetadata(sourceId);
|
|
1375
|
+
server.registerTool(
|
|
1376
|
+
metadata.name,
|
|
1377
|
+
{
|
|
1378
|
+
description: metadata.description,
|
|
1379
|
+
inputSchema: explainSqlInputSchema,
|
|
1380
|
+
annotations: metadata.annotations
|
|
1381
|
+
},
|
|
1382
|
+
createExplainSqlToolHandler(sourceId)
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
function registerHealthCheckTool(server, sourceId) {
|
|
1386
|
+
const metadata = getHealthCheckMetadata(sourceId);
|
|
1387
|
+
server.registerTool(
|
|
1388
|
+
metadata.name,
|
|
1389
|
+
{
|
|
1390
|
+
description: metadata.description,
|
|
1391
|
+
inputSchema: healthCheckInputSchema,
|
|
1392
|
+
annotations: metadata.annotations
|
|
1393
|
+
},
|
|
1394
|
+
createHealthCheckToolHandler(sourceId)
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1215
1397
|
function registerCustomTool(server, sourceId, toolConfig) {
|
|
1216
1398
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
1217
1399
|
const dbType = sourceConfig.type;
|
|
1218
|
-
const isReadOnly =
|
|
1219
|
-
const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
|
|
1400
|
+
const isReadOnly = classifySQL(toolConfig.statement, dbType) === "read";
|
|
1220
1401
|
server.registerTool(
|
|
1221
1402
|
toolConfig.name,
|
|
1222
1403
|
{
|
|
1223
1404
|
description: toolConfig.description,
|
|
1224
|
-
inputSchema:
|
|
1405
|
+
inputSchema: getCustomToolInputSchema(toolConfig),
|
|
1225
1406
|
annotations: {
|
|
1226
1407
|
title: `${toolConfig.name} (${dbType})`,
|
|
1227
1408
|
readOnlyHint: isReadOnly,
|
|
@@ -1654,6 +1835,32 @@ function validateOrigin(originHeader, hostHeader, allowedHosts) {
|
|
|
1654
1835
|
return { ok: true };
|
|
1655
1836
|
}
|
|
1656
1837
|
|
|
1838
|
+
// src/utils/auth-token.ts
|
|
1839
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1840
|
+
var BEARER_PREFIX = "Bearer ";
|
|
1841
|
+
function constantTimeEqual(a, b) {
|
|
1842
|
+
const bufA = Buffer.from(a);
|
|
1843
|
+
const bufB = Buffer.from(b);
|
|
1844
|
+
if (bufA.length !== bufB.length) return false;
|
|
1845
|
+
return timingSafeEqual(bufA, bufB);
|
|
1846
|
+
}
|
|
1847
|
+
function validateAuthToken(authorizationHeader, tokens) {
|
|
1848
|
+
if (tokens.length === 0) return { ok: true };
|
|
1849
|
+
if (!authorizationHeader || !authorizationHeader.startsWith(BEARER_PREFIX)) {
|
|
1850
|
+
return {
|
|
1851
|
+
ok: false,
|
|
1852
|
+
status: 401,
|
|
1853
|
+
message: "Missing or malformed Authorization header. Expected: Bearer <token>"
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
const presented = authorizationHeader.slice(BEARER_PREFIX.length);
|
|
1857
|
+
const matches = tokens.some((token) => constantTimeEqual(presented, token));
|
|
1858
|
+
if (!matches) {
|
|
1859
|
+
return { ok: false, status: 401, message: "Invalid bearer token" };
|
|
1860
|
+
}
|
|
1861
|
+
return { ok: true };
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1657
1864
|
// src/server.ts
|
|
1658
1865
|
var __filename = fileURLToPath(import.meta.url);
|
|
1659
1866
|
var __dirname = path.dirname(__filename);
|
|
@@ -1661,6 +1868,8 @@ var packageJsonPath = path.join(__dirname, "..", "package.json");
|
|
|
1661
1868
|
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1662
1869
|
var SERVER_NAME = "DBHub MCP Server";
|
|
1663
1870
|
var SERVER_VERSION = packageJson.version;
|
|
1871
|
+
var TOOLS_LIST_CACHE_TTL_MS = 3e5;
|
|
1872
|
+
var logMcpError = (scope) => (error) => console.error(`MCP ${scope} error:`, error);
|
|
1664
1873
|
function generateBanner(version, modes = []) {
|
|
1665
1874
|
const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
|
|
1666
1875
|
return `
|
|
@@ -1706,7 +1915,7 @@ See documentation for more details on configuring database connections.
|
|
|
1706
1915
|
const sources = sourceConfigsData.sources;
|
|
1707
1916
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1708
1917
|
await connectorManager.connectWithSources(sources);
|
|
1709
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1918
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-WVSFVS35.js");
|
|
1710
1919
|
initializeToolRegistry2({
|
|
1711
1920
|
sources: sourceConfigsData.sources,
|
|
1712
1921
|
tools: sourceConfigsData.tools
|
|
@@ -1717,10 +1926,18 @@ See documentation for more details on configuring database connections.
|
|
|
1717
1926
|
initialTools: sourceConfigsData.tools
|
|
1718
1927
|
});
|
|
1719
1928
|
const createServer = () => {
|
|
1720
|
-
const server = new McpServer(
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1929
|
+
const server = new McpServer(
|
|
1930
|
+
{
|
|
1931
|
+
name: SERVER_NAME,
|
|
1932
|
+
version: SERVER_VERSION
|
|
1933
|
+
},
|
|
1934
|
+
{
|
|
1935
|
+
// `private` keeps caching client-side (no shared/proxy caches).
|
|
1936
|
+
cacheHints: {
|
|
1937
|
+
"tools/list": { ttlMs: TOOLS_LIST_CACHE_TTL_MS, cacheScope: "private" }
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
);
|
|
1724
1941
|
registerTools(server);
|
|
1725
1942
|
return server;
|
|
1726
1943
|
};
|
|
@@ -1728,6 +1945,7 @@ See documentation for more details on configuring database connections.
|
|
|
1728
1945
|
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
1729
1946
|
const host = transportData.type === "http" ? resolveHost().host : null;
|
|
1730
1947
|
const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
|
|
1948
|
+
const { tokens: authTokens, source: authTokenSource } = resolveAuthTokens();
|
|
1731
1949
|
const activeModes = [];
|
|
1732
1950
|
const modeDescriptions = [];
|
|
1733
1951
|
const isDemo = isDemoMode();
|
|
@@ -1762,44 +1980,33 @@ See documentation for more details on configuring database connections.
|
|
|
1762
1980
|
}
|
|
1763
1981
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1764
1982
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1765
|
-
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
1983
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1766
1984
|
res.header("Access-Control-Allow-Credentials", "true");
|
|
1767
1985
|
if (req.method === "OPTIONS") {
|
|
1768
1986
|
return res.sendStatus(200);
|
|
1769
1987
|
}
|
|
1770
1988
|
next();
|
|
1771
1989
|
});
|
|
1772
|
-
const frontendPath = path.join(__dirname, "public");
|
|
1773
|
-
app.use(express.static(frontendPath));
|
|
1774
1990
|
app.get("/healthz", (req, res) => {
|
|
1775
1991
|
res.status(200).send("OK");
|
|
1776
1992
|
});
|
|
1993
|
+
app.use((req, res, next) => {
|
|
1994
|
+
const result = validateAuthToken(req.headers.authorization, authTokens);
|
|
1995
|
+
if (!result.ok) {
|
|
1996
|
+
res.header("WWW-Authenticate", "Bearer");
|
|
1997
|
+
return res.status(result.status).json({ error: "Unauthorized", message: result.message });
|
|
1998
|
+
}
|
|
1999
|
+
next();
|
|
2000
|
+
});
|
|
2001
|
+
const frontendPath = path.join(__dirname, "public");
|
|
2002
|
+
app.use(express.static(frontendPath));
|
|
1777
2003
|
app.get("/api/sources", listSources);
|
|
1778
2004
|
app.get("/api/sources/:sourceId", getSource);
|
|
1779
2005
|
app.get("/api/requests", listRequests);
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
});
|
|
1785
|
-
});
|
|
1786
|
-
app.post("/mcp", async (req, res) => {
|
|
1787
|
-
try {
|
|
1788
|
-
const transport = new StreamableHTTPServerTransport({
|
|
1789
|
-
sessionIdGenerator: void 0,
|
|
1790
|
-
// Disable session management for stateless mode
|
|
1791
|
-
enableJsonResponse: true
|
|
1792
|
-
// Use JSON responses (SSE not supported in stateless mode)
|
|
1793
|
-
});
|
|
1794
|
-
const server = createServer();
|
|
1795
|
-
await server.connect(transport);
|
|
1796
|
-
await transport.handleRequest(req, res, req.body);
|
|
1797
|
-
} catch (error) {
|
|
1798
|
-
console.error("Error handling request:", error);
|
|
1799
|
-
if (!res.headersSent) {
|
|
1800
|
-
res.status(500).json({ error: "Internal server error" });
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
2006
|
+
const mcpHandler = createMcpHandler(createServer, { onerror: logMcpError("handler") });
|
|
2007
|
+
const mcpNodeHandler = toNodeHandler(mcpHandler, { onerror: logMcpError("adapter") });
|
|
2008
|
+
app.all("/mcp", (req, res) => {
|
|
2009
|
+
void mcpNodeHandler(req, res, req.body);
|
|
1803
2010
|
});
|
|
1804
2011
|
if (process.env.NODE_ENV !== "development") {
|
|
1805
2012
|
app.get("*", (req, res) => {
|
|
@@ -1824,6 +2031,11 @@ See documentation for more details on configuring database connections.
|
|
|
1824
2031
|
} else {
|
|
1825
2032
|
console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
|
|
1826
2033
|
}
|
|
2034
|
+
if (authTokens.length > 0) {
|
|
2035
|
+
console.error(`Auth: bearer token required (${authTokens.length} token(s) configured via ${authTokenSource})`);
|
|
2036
|
+
} else {
|
|
2037
|
+
console.error("Auth: disabled (set --auth-token or DBHUB_AUTH_TOKEN to require a bearer token)");
|
|
2038
|
+
}
|
|
1827
2039
|
if (process.env.NODE_ENV === "development") {
|
|
1828
2040
|
console.error("Development mode detected!");
|
|
1829
2041
|
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
@@ -1835,16 +2047,14 @@ See documentation for more details on configuring database connections.
|
|
|
1835
2047
|
console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
|
|
1836
2048
|
});
|
|
1837
2049
|
} else {
|
|
1838
|
-
const
|
|
1839
|
-
const transport = new StdioServerTransport();
|
|
1840
|
-
await server.connect(transport);
|
|
2050
|
+
const stdioHandle = serveStdio(createServer, { onerror: logMcpError("stdio") });
|
|
1841
2051
|
console.error("MCP server running on stdio");
|
|
1842
2052
|
let isShuttingDown = false;
|
|
1843
2053
|
const shutdown = async () => {
|
|
1844
2054
|
if (isShuttingDown) return;
|
|
1845
2055
|
isShuttingDown = true;
|
|
1846
2056
|
console.error("Shutting down...");
|
|
1847
|
-
await
|
|
2057
|
+
await stdioHandle.close();
|
|
1848
2058
|
await connectorManager.disconnect();
|
|
1849
2059
|
process.exit(0);
|
|
1850
2060
|
};
|
|
@@ -1860,12 +2070,12 @@ See documentation for more details on configuring database connections.
|
|
|
1860
2070
|
|
|
1861
2071
|
// src/index.ts
|
|
1862
2072
|
var connectorModules = [
|
|
1863
|
-
{ load: () => import("./postgres-
|
|
1864
|
-
{ load: () => import("./sqlserver-
|
|
1865
|
-
{ load: () => import("./
|
|
1866
|
-
{ load: () => import("./
|
|
1867
|
-
{ load: () => import("./
|
|
1868
|
-
{ load: () => import("./
|
|
2073
|
+
{ load: () => import("./postgres-C24SBQ4I.js"), name: "PostgreSQL", driver: "pg" },
|
|
2074
|
+
{ load: () => import("./sqlserver-KZPPK67Q.js"), name: "SQL Server", driver: "mssql" },
|
|
2075
|
+
{ load: () => import("./dameng-NKF7NZKE.js"), name: "Dameng", driver: "dmdb" },
|
|
2076
|
+
{ load: () => import("./sqlite-7BNGDMBZ.js"), name: "SQLite", driver: "node:sqlite" },
|
|
2077
|
+
{ load: () => import("./mysql-TESH7KKV.js"), name: "MySQL", driver: "mysql2" },
|
|
2078
|
+
{ load: () => import("./mariadb-MMC6WMEG.js"), name: "MariaDB", driver: "mariadb" }
|
|
1869
2079
|
];
|
|
1870
2080
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1871
2081
|
console.error("Fatal error:", error);
|