@bytebase/dbhub 1.0.0 → 1.2.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 +10 -52
- package/dist/{chunk-MQR6VX5F.js → chunk-CUBIR72I.js} +30 -5
- package/dist/chunk-FU2ZJE4E.js +16 -0
- package/dist/{chunk-JO7YQPBQ.js → chunk-PBN2UXB5.js} +25 -3
- package/dist/chunk-QEFMUQJ2.js +179 -0
- package/dist/{chunk-S2V6RZ3C.js → chunk-RSB7DPO2.js} +64 -3
- package/dist/{chunk-JEZZN2YZ.js → chunk-WIQ4DWGY.js} +16 -0
- package/dist/index.js +341 -57
- package/dist/{mariadb-72C2BNCG.js → mariadb-E33EXGJZ.js} +16 -9
- package/dist/{mysql-CDUKJMNN.js → mysql-RLDQI3SJ.js} +68 -11
- package/dist/{postgres-YAYHDXVD.js → postgres-6XY2GPQV.js} +80 -13
- package/dist/public/assets/{index-DBYlgGks.js → index-DOZ5HOZ9.js} +21 -20
- package/dist/public/index.html +1 -1
- package/dist/{registry-ZU6ZFNQ2.js → registry-XMEJF7WW.js} +2 -2
- package/dist/{sqlite-5M7SDAQZ.js → sqlite-CZE6Z36Y.js} +14 -13
- package/dist/{sqlserver-COKW7RIK.js → sqlserver-IIGRXGYK.js} +148 -13
- package/package.json +4 -2
- package/dist/chunk-DUCNPMNW.js +0 -113
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,18 +12,20 @@ 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-CUBIR72I.js";
|
|
19
22
|
import {
|
|
20
23
|
allowedKeywords,
|
|
24
|
+
getFirstKeyword,
|
|
25
|
+
hasEscapeHatchFunction,
|
|
21
26
|
isReadOnlySQL,
|
|
22
|
-
sqlServerDynamicSqlPattern
|
|
23
|
-
|
|
24
|
-
} from "./chunk-JO7YQPBQ.js";
|
|
27
|
+
sqlServerDynamicSqlPattern
|
|
28
|
+
} from "./chunk-PBN2UXB5.js";
|
|
25
29
|
import {
|
|
26
30
|
loadConnectors
|
|
27
31
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -35,7 +39,7 @@ import {
|
|
|
35
39
|
parseConnectionInfoFromDSN,
|
|
36
40
|
splitSQLStatements,
|
|
37
41
|
stripCommentsAndStrings
|
|
38
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-WIQ4DWGY.js";
|
|
39
43
|
|
|
40
44
|
// src/server.ts
|
|
41
45
|
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
|
|
@@ -114,7 +118,8 @@ var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|d
|
|
|
114
118
|
var grantRevokePattern = /\b(?:grant|revoke)\b/i;
|
|
115
119
|
function isAdminStatement(stripped, connectorType) {
|
|
116
120
|
if (grantRevokePattern.test(stripped)) return true;
|
|
117
|
-
|
|
121
|
+
if (hasEscapeHatchFunction(stripped, connectorType)) return true;
|
|
122
|
+
return connectorType === "sqlserver" && sqlServerDynamicSqlPattern.test(stripped);
|
|
118
123
|
}
|
|
119
124
|
function policyFromReadonly(readonly) {
|
|
120
125
|
const write = readonly === true ? "deny" : "allow";
|
|
@@ -206,10 +211,29 @@ function getClientIdentifier(ctx) {
|
|
|
206
211
|
return "stdio";
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
// src/utils/normalize-id.ts
|
|
215
|
+
function normalizeSourceId(id) {
|
|
216
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
217
|
+
}
|
|
218
|
+
|
|
209
219
|
// src/utils/tool-handler-helpers.ts
|
|
210
220
|
function getEffectiveSourceId(sourceId) {
|
|
211
221
|
return sourceId || "default";
|
|
212
222
|
}
|
|
223
|
+
function toStatementsPayload(resultSets) {
|
|
224
|
+
return resultSets.map((set) => ({
|
|
225
|
+
sql: set.sql,
|
|
226
|
+
rows: set.rows,
|
|
227
|
+
count: set.rowCount
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
function resolveTrackedToolName(sourceId, baseName) {
|
|
231
|
+
const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
|
|
232
|
+
if (isSingleSource) {
|
|
233
|
+
return baseName;
|
|
234
|
+
}
|
|
235
|
+
return `${baseName}_${normalizeSourceId(getEffectiveSourceId(sourceId))}`;
|
|
236
|
+
}
|
|
213
237
|
function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
|
|
214
238
|
return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
|
|
215
239
|
}
|
|
@@ -272,8 +296,7 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
272
296
|
};
|
|
273
297
|
result = await connector.executeSQL(sql, executeOptions);
|
|
274
298
|
const responseData = {
|
|
275
|
-
|
|
276
|
-
count: result.rowCount,
|
|
299
|
+
statements: toStatementsPayload(result.resultSets),
|
|
277
300
|
source_id: effectiveSourceId,
|
|
278
301
|
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
279
302
|
};
|
|
@@ -332,8 +355,9 @@ async function getTableRowCount(connector, tableName, schemaName) {
|
|
|
332
355
|
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
333
356
|
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
334
357
|
const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
|
|
335
|
-
|
|
336
|
-
|
|
358
|
+
const rows = result.resultSets[0]?.rows;
|
|
359
|
+
if (rows && rows.length > 0) {
|
|
360
|
+
return Number(rows[0].count || rows[0].COUNT || 0);
|
|
337
361
|
}
|
|
338
362
|
} catch (error) {
|
|
339
363
|
return null;
|
|
@@ -785,15 +809,151 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
785
809
|
};
|
|
786
810
|
}
|
|
787
811
|
|
|
788
|
-
// src/
|
|
812
|
+
// src/tools/explain-sql.ts
|
|
789
813
|
import { z as z3 } from "zod";
|
|
814
|
+
var explainSqlSchema = {
|
|
815
|
+
sql: z3.string().describe("Single SQL statement to explain (no trailing semicolon-separated statements)")
|
|
816
|
+
};
|
|
817
|
+
var explainSqlInputSchema = z3.object(explainSqlSchema);
|
|
818
|
+
function buildExplainStatement(connectorType, sql) {
|
|
819
|
+
if (connectorType === "sqlite") {
|
|
820
|
+
return `EXPLAIN QUERY PLAN ${sql}`;
|
|
821
|
+
}
|
|
822
|
+
return `EXPLAIN ${sql}`;
|
|
823
|
+
}
|
|
824
|
+
function validateExplainInput(sql, connectorType) {
|
|
825
|
+
const statements = splitSQLStatements(sql, connectorType);
|
|
826
|
+
if (statements.length !== 1) {
|
|
827
|
+
return "explain_sql only supports a single SQL statement";
|
|
828
|
+
}
|
|
829
|
+
const firstWord = getFirstKeyword(statements[0], connectorType);
|
|
830
|
+
if (firstWord === "explain") {
|
|
831
|
+
return "explain_sql input must not itself start with EXPLAIN";
|
|
832
|
+
}
|
|
833
|
+
if (firstWord === "analyze") {
|
|
834
|
+
return "explain_sql does not support ANALYZE (it must never execute the statement)";
|
|
835
|
+
}
|
|
836
|
+
const cleaned = stripCommentsAndStrings(statements[0], connectorType).trim();
|
|
837
|
+
const leadingOptions = cleaned.match(/^\(([\s\S]*?)\)/)?.[1];
|
|
838
|
+
if (leadingOptions && /\banalyze\b/i.test(leadingOptions)) {
|
|
839
|
+
return "explain_sql does not support ANALYZE (it must never execute the statement)";
|
|
840
|
+
}
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
function createExplainSqlToolHandler(sourceId) {
|
|
844
|
+
return async (args, extra) => {
|
|
845
|
+
const { sql } = args;
|
|
846
|
+
const startTime = Date.now();
|
|
847
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
848
|
+
let success = true;
|
|
849
|
+
let errorMessage;
|
|
850
|
+
let result;
|
|
851
|
+
try {
|
|
852
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
853
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
854
|
+
const validationError = validateExplainInput(sql, connector.id);
|
|
855
|
+
if (validationError) {
|
|
856
|
+
success = false;
|
|
857
|
+
errorMessage = validationError;
|
|
858
|
+
return createToolErrorResponse(errorMessage, "INVALID_INPUT");
|
|
859
|
+
}
|
|
860
|
+
const explainStatement = buildExplainStatement(connector.id, sql);
|
|
861
|
+
result = await connector.executeSQL(explainStatement, { readonly: true });
|
|
862
|
+
const responseData = {
|
|
863
|
+
rows: result.resultSets[0].rows,
|
|
864
|
+
count: result.resultSets[0].rowCount,
|
|
865
|
+
source_id: effectiveSourceId,
|
|
866
|
+
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
867
|
+
};
|
|
868
|
+
return createToolSuccessResponse(responseData);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
success = false;
|
|
871
|
+
errorMessage = error.message;
|
|
872
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
873
|
+
if (classified) return classified;
|
|
874
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
875
|
+
} finally {
|
|
876
|
+
trackToolRequest(
|
|
877
|
+
{
|
|
878
|
+
sourceId: effectiveSourceId,
|
|
879
|
+
toolName: resolveTrackedToolName(sourceId, "explain_sql"),
|
|
880
|
+
sql
|
|
881
|
+
},
|
|
882
|
+
startTime,
|
|
883
|
+
extra,
|
|
884
|
+
success,
|
|
885
|
+
errorMessage
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
}
|
|
790
890
|
|
|
791
|
-
// src/
|
|
792
|
-
|
|
793
|
-
|
|
891
|
+
// src/tools/health-check.ts
|
|
892
|
+
import { z as z4 } from "zod";
|
|
893
|
+
var healthCheckSchema = {};
|
|
894
|
+
var healthCheckInputSchema = z4.object(healthCheckSchema);
|
|
895
|
+
function createHealthCheckToolHandler(sourceId) {
|
|
896
|
+
return async (_args, extra) => {
|
|
897
|
+
const startTime = Date.now();
|
|
898
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
899
|
+
let success = true;
|
|
900
|
+
let errorMessage;
|
|
901
|
+
try {
|
|
902
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
903
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
904
|
+
if (!connector.getHealthCheck) {
|
|
905
|
+
success = false;
|
|
906
|
+
errorMessage = `health_check is not supported for '${connector.id}' sources yet`;
|
|
907
|
+
return createToolErrorResponse(errorMessage, "UNSUPPORTED");
|
|
908
|
+
}
|
|
909
|
+
const health = await connector.getHealthCheck();
|
|
910
|
+
return createToolSuccessResponse({
|
|
911
|
+
source_id: effectiveSourceId,
|
|
912
|
+
...health.connections ? {
|
|
913
|
+
connections: {
|
|
914
|
+
total: health.connections.total,
|
|
915
|
+
active: health.connections.active,
|
|
916
|
+
idle: health.connections.idle,
|
|
917
|
+
idle_in_transaction: health.connections.idleInTransaction,
|
|
918
|
+
...health.connections.idleInTransactionAborted !== void 0 ? { idle_in_transaction_aborted: health.connections.idleInTransactionAborted } : {},
|
|
919
|
+
max_connections: health.connections.maxConnections,
|
|
920
|
+
longest_idle_in_transaction_seconds: health.connections.longestIdleInTransactionSeconds,
|
|
921
|
+
longest_active_query_seconds: health.connections.longestActiveQuerySeconds
|
|
922
|
+
}
|
|
923
|
+
} : {},
|
|
924
|
+
...health.bufferCache ? {
|
|
925
|
+
buffer_cache: {
|
|
926
|
+
hit_ratio_pct: health.bufferCache.hitRatioPct,
|
|
927
|
+
blocks_hit: health.bufferCache.blocksHit,
|
|
928
|
+
blocks_read: health.bufferCache.blocksRead
|
|
929
|
+
}
|
|
930
|
+
} : {},
|
|
931
|
+
...health.notes && health.notes.length > 0 ? { notes: health.notes } : {}
|
|
932
|
+
});
|
|
933
|
+
} catch (error) {
|
|
934
|
+
success = false;
|
|
935
|
+
errorMessage = error.message;
|
|
936
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
937
|
+
if (classified) return classified;
|
|
938
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
939
|
+
} finally {
|
|
940
|
+
trackToolRequest(
|
|
941
|
+
{
|
|
942
|
+
sourceId: effectiveSourceId,
|
|
943
|
+
toolName: effectiveSourceId === "default" ? "health_check" : `health_check_${effectiveSourceId}`,
|
|
944
|
+
sql: ""
|
|
945
|
+
},
|
|
946
|
+
startTime,
|
|
947
|
+
extra,
|
|
948
|
+
success,
|
|
949
|
+
errorMessage
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
};
|
|
794
953
|
}
|
|
795
954
|
|
|
796
955
|
// src/utils/tool-metadata.ts
|
|
956
|
+
import { z as z5 } from "zod";
|
|
797
957
|
function buildSourceDescriptionPrefix(description) {
|
|
798
958
|
const trimmed = description?.trim() ?? "";
|
|
799
959
|
if (!trimmed) return "";
|
|
@@ -803,17 +963,17 @@ function zodToParameters(schema) {
|
|
|
803
963
|
const parameters = [];
|
|
804
964
|
for (const [key, zodType] of Object.entries(schema)) {
|
|
805
965
|
const description = zodType.description || "";
|
|
806
|
-
const required = !(zodType instanceof
|
|
966
|
+
const required = !(zodType instanceof z5.ZodOptional);
|
|
807
967
|
let type = "string";
|
|
808
|
-
if (zodType instanceof
|
|
968
|
+
if (zodType instanceof z5.ZodString) {
|
|
809
969
|
type = "string";
|
|
810
|
-
} else if (zodType instanceof
|
|
970
|
+
} else if (zodType instanceof z5.ZodNumber) {
|
|
811
971
|
type = "number";
|
|
812
|
-
} else if (zodType instanceof
|
|
972
|
+
} else if (zodType instanceof z5.ZodBoolean) {
|
|
813
973
|
type = "boolean";
|
|
814
|
-
} else if (zodType instanceof
|
|
974
|
+
} else if (zodType instanceof z5.ZodArray) {
|
|
815
975
|
type = "array";
|
|
816
|
-
} else if (zodType instanceof
|
|
976
|
+
} else if (zodType instanceof z5.ZodObject) {
|
|
817
977
|
type = "object";
|
|
818
978
|
}
|
|
819
979
|
parameters.push({
|
|
@@ -825,20 +985,28 @@ function zodToParameters(schema) {
|
|
|
825
985
|
}
|
|
826
986
|
return parameters;
|
|
827
987
|
}
|
|
828
|
-
function
|
|
988
|
+
function resolveBuiltinToolNaming(sourceId, baseName, titleLabel) {
|
|
829
989
|
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
830
990
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
831
991
|
const dbType = sourceConfig.type;
|
|
832
992
|
const isSingleSource = sourceIds.length === 1;
|
|
993
|
+
const name = isSingleSource ? baseName : `${baseName}_${normalizeSourceId(sourceId)}`;
|
|
994
|
+
const title = isSingleSource ? `${titleLabel} (${dbType})` : `${titleLabel} on ${sourceId} (${dbType})`;
|
|
995
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
996
|
+
return { dbType, isSingleSource, name, title, userDescPrefix };
|
|
997
|
+
}
|
|
998
|
+
function getExecuteSqlMetadata(sourceId) {
|
|
999
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1000
|
+
sourceId,
|
|
1001
|
+
"execute_sql",
|
|
1002
|
+
"Execute SQL"
|
|
1003
|
+
);
|
|
833
1004
|
const registry = getToolRegistry();
|
|
834
1005
|
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
|
|
835
1006
|
const executeOptions = {
|
|
836
1007
|
readonly: toolConfig?.readonly,
|
|
837
1008
|
maxRows: toolConfig?.max_rows
|
|
838
1009
|
};
|
|
839
|
-
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
840
|
-
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
841
|
-
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
842
1010
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
843
1011
|
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
844
1012
|
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
@@ -855,25 +1023,62 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
855
1023
|
openWorldHint: false
|
|
856
1024
|
};
|
|
857
1025
|
return {
|
|
858
|
-
name
|
|
1026
|
+
name,
|
|
859
1027
|
description,
|
|
860
1028
|
schema: executeSqlSchema,
|
|
861
1029
|
annotations
|
|
862
1030
|
};
|
|
863
1031
|
}
|
|
864
1032
|
function getSearchObjectsMetadata(sourceId) {
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
871
|
-
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
1033
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1034
|
+
sourceId,
|
|
1035
|
+
"search_objects",
|
|
1036
|
+
"Search Database Objects"
|
|
1037
|
+
);
|
|
872
1038
|
const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
|
|
1039
|
+
return { name, description, title };
|
|
1040
|
+
}
|
|
1041
|
+
function getExplainSqlMetadata(sourceId) {
|
|
1042
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1043
|
+
sourceId,
|
|
1044
|
+
"explain_sql",
|
|
1045
|
+
"Explain Query Plan"
|
|
1046
|
+
);
|
|
1047
|
+
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).`;
|
|
1048
|
+
const annotations = {
|
|
1049
|
+
title,
|
|
1050
|
+
readOnlyHint: true,
|
|
1051
|
+
destructiveHint: false,
|
|
1052
|
+
idempotentHint: true,
|
|
1053
|
+
openWorldHint: false
|
|
1054
|
+
};
|
|
1055
|
+
return {
|
|
1056
|
+
name,
|
|
1057
|
+
description,
|
|
1058
|
+
schema: explainSqlSchema,
|
|
1059
|
+
annotations
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function getHealthCheckMetadata(sourceId) {
|
|
1063
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1064
|
+
sourceId,
|
|
1065
|
+
"health_check",
|
|
1066
|
+
"Health Check"
|
|
1067
|
+
);
|
|
1068
|
+
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).`;
|
|
1069
|
+
const annotations = {
|
|
1070
|
+
title,
|
|
1071
|
+
readOnlyHint: true,
|
|
1072
|
+
destructiveHint: false,
|
|
1073
|
+
idempotentHint: false,
|
|
1074
|
+
// Metrics change from call to call, unlike explain_sql's plan output
|
|
1075
|
+
openWorldHint: false
|
|
1076
|
+
};
|
|
873
1077
|
return {
|
|
874
|
-
name
|
|
1078
|
+
name,
|
|
875
1079
|
description,
|
|
876
|
-
|
|
1080
|
+
schema: healthCheckSchema,
|
|
1081
|
+
annotations
|
|
877
1082
|
};
|
|
878
1083
|
}
|
|
879
1084
|
function customParamsToToolParams(params) {
|
|
@@ -910,13 +1115,13 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
910
1115
|
name: "object_type",
|
|
911
1116
|
type: "string",
|
|
912
1117
|
required: true,
|
|
913
|
-
description: "Object type to search
|
|
1118
|
+
description: "Object type to search"
|
|
914
1119
|
},
|
|
915
1120
|
{
|
|
916
1121
|
name: "pattern",
|
|
917
1122
|
type: "string",
|
|
918
1123
|
required: false,
|
|
919
|
-
description: "LIKE pattern (% = any chars, _ = one char)
|
|
1124
|
+
description: "LIKE pattern (% = any chars, _ = one char)"
|
|
920
1125
|
},
|
|
921
1126
|
{
|
|
922
1127
|
name: "schema",
|
|
@@ -940,13 +1145,23 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
940
1145
|
name: "limit",
|
|
941
1146
|
type: "integer",
|
|
942
1147
|
required: false,
|
|
943
|
-
description: "Max results
|
|
1148
|
+
description: "Max results"
|
|
944
1149
|
}
|
|
945
1150
|
],
|
|
946
1151
|
readonly: true
|
|
947
1152
|
// search_objects is always readonly
|
|
948
1153
|
};
|
|
949
1154
|
}
|
|
1155
|
+
function buildExplainSqlTool(sourceId) {
|
|
1156
|
+
const explainMetadata = getExplainSqlMetadata(sourceId);
|
|
1157
|
+
return {
|
|
1158
|
+
name: explainMetadata.name,
|
|
1159
|
+
description: explainMetadata.description,
|
|
1160
|
+
parameters: zodToParameters(explainMetadata.schema),
|
|
1161
|
+
readonly: true
|
|
1162
|
+
// explain_sql is always readonly
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
950
1165
|
function buildCustomTool(toolConfig) {
|
|
951
1166
|
return {
|
|
952
1167
|
name: toolConfig.name,
|
|
@@ -965,6 +1180,8 @@ function getToolsForSource(sourceId) {
|
|
|
965
1180
|
return buildExecuteSqlTool(sourceId, toolConfig);
|
|
966
1181
|
} else if (toolConfig.name === "search_objects") {
|
|
967
1182
|
return buildSearchObjectsTool(sourceId);
|
|
1183
|
+
} else if (toolConfig.name === "explain_sql") {
|
|
1184
|
+
return buildExplainSqlTool(sourceId);
|
|
968
1185
|
} else {
|
|
969
1186
|
return buildCustomTool(toolConfig);
|
|
970
1187
|
}
|
|
@@ -972,7 +1189,7 @@ function getToolsForSource(sourceId) {
|
|
|
972
1189
|
}
|
|
973
1190
|
|
|
974
1191
|
// src/tools/custom-tool-handler.ts
|
|
975
|
-
import { z as
|
|
1192
|
+
import { z as z6 } from "zod";
|
|
976
1193
|
function buildZodSchemaFromParameters(parameters) {
|
|
977
1194
|
if (!parameters || parameters.length === 0) {
|
|
978
1195
|
return {};
|
|
@@ -982,26 +1199,26 @@ function buildZodSchemaFromParameters(parameters) {
|
|
|
982
1199
|
let fieldSchema;
|
|
983
1200
|
switch (param.type) {
|
|
984
1201
|
case "string":
|
|
985
|
-
fieldSchema =
|
|
1202
|
+
fieldSchema = z6.string().describe(param.description);
|
|
986
1203
|
break;
|
|
987
1204
|
case "integer":
|
|
988
|
-
fieldSchema =
|
|
1205
|
+
fieldSchema = z6.number().int().describe(param.description);
|
|
989
1206
|
break;
|
|
990
1207
|
case "float":
|
|
991
|
-
fieldSchema =
|
|
1208
|
+
fieldSchema = z6.number().describe(param.description);
|
|
992
1209
|
break;
|
|
993
1210
|
case "boolean":
|
|
994
|
-
fieldSchema =
|
|
1211
|
+
fieldSchema = z6.boolean().describe(param.description);
|
|
995
1212
|
break;
|
|
996
1213
|
case "array":
|
|
997
|
-
fieldSchema =
|
|
1214
|
+
fieldSchema = z6.array(z6.unknown()).describe(param.description);
|
|
998
1215
|
break;
|
|
999
1216
|
default:
|
|
1000
1217
|
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
1001
1218
|
}
|
|
1002
1219
|
if (param.allowed_values && param.allowed_values.length > 0) {
|
|
1003
1220
|
if (param.type === "string") {
|
|
1004
|
-
fieldSchema =
|
|
1221
|
+
fieldSchema = z6.enum(param.allowed_values).describe(param.description);
|
|
1005
1222
|
} else {
|
|
1006
1223
|
fieldSchema = fieldSchema.refine(
|
|
1007
1224
|
(val) => param.allowed_values.includes(val),
|
|
@@ -1022,7 +1239,7 @@ var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
|
|
|
1022
1239
|
function getCustomToolInputSchema(toolConfig) {
|
|
1023
1240
|
let schema = customToolInputSchemas.get(toolConfig);
|
|
1024
1241
|
if (!schema) {
|
|
1025
|
-
schema =
|
|
1242
|
+
schema = z6.object(buildZodSchemaFromParameters(toolConfig.parameters));
|
|
1026
1243
|
customToolInputSchemas.set(toolConfig, schema);
|
|
1027
1244
|
}
|
|
1028
1245
|
return schema;
|
|
@@ -1058,8 +1275,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1058
1275
|
paramValues
|
|
1059
1276
|
);
|
|
1060
1277
|
const responseData = {
|
|
1061
|
-
|
|
1062
|
-
count: result.rowCount,
|
|
1278
|
+
statements: toStatementsPayload(result.resultSets),
|
|
1063
1279
|
source_id: toolConfig.source
|
|
1064
1280
|
};
|
|
1065
1281
|
return createToolSuccessResponse(responseData);
|
|
@@ -1068,7 +1284,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1068
1284
|
errorMessage = error.message;
|
|
1069
1285
|
const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
|
|
1070
1286
|
if (classified) return classified;
|
|
1071
|
-
if (error instanceof
|
|
1287
|
+
if (error instanceof z6.ZodError) {
|
|
1072
1288
|
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1073
1289
|
errorMessage = `Parameter validation failed: ${issues}`;
|
|
1074
1290
|
} else {
|
|
@@ -1108,6 +1324,10 @@ function registerTools(server) {
|
|
|
1108
1324
|
registerExecuteSqlTool(server, sourceId);
|
|
1109
1325
|
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
1110
1326
|
registerSearchObjectsTool(server, sourceId);
|
|
1327
|
+
} else if (toolConfig.name === BUILTIN_TOOL_EXPLAIN_SQL) {
|
|
1328
|
+
registerExplainSqlTool(server, sourceId);
|
|
1329
|
+
} else if (toolConfig.name === BUILTIN_TOOL_HEALTH_CHECK) {
|
|
1330
|
+
registerHealthCheckTool(server, sourceId);
|
|
1111
1331
|
} else {
|
|
1112
1332
|
registerCustomTool(server, sourceId, toolConfig);
|
|
1113
1333
|
}
|
|
@@ -1144,6 +1364,30 @@ function registerSearchObjectsTool(server, sourceId) {
|
|
|
1144
1364
|
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
1145
1365
|
);
|
|
1146
1366
|
}
|
|
1367
|
+
function registerExplainSqlTool(server, sourceId) {
|
|
1368
|
+
const metadata = getExplainSqlMetadata(sourceId);
|
|
1369
|
+
server.registerTool(
|
|
1370
|
+
metadata.name,
|
|
1371
|
+
{
|
|
1372
|
+
description: metadata.description,
|
|
1373
|
+
inputSchema: explainSqlInputSchema,
|
|
1374
|
+
annotations: metadata.annotations
|
|
1375
|
+
},
|
|
1376
|
+
createExplainSqlToolHandler(sourceId)
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
function registerHealthCheckTool(server, sourceId) {
|
|
1380
|
+
const metadata = getHealthCheckMetadata(sourceId);
|
|
1381
|
+
server.registerTool(
|
|
1382
|
+
metadata.name,
|
|
1383
|
+
{
|
|
1384
|
+
description: metadata.description,
|
|
1385
|
+
inputSchema: healthCheckInputSchema,
|
|
1386
|
+
annotations: metadata.annotations
|
|
1387
|
+
},
|
|
1388
|
+
createHealthCheckToolHandler(sourceId)
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1147
1391
|
function registerCustomTool(server, sourceId, toolConfig) {
|
|
1148
1392
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
1149
1393
|
const dbType = sourceConfig.type;
|
|
@@ -1585,6 +1829,32 @@ function validateOrigin(originHeader, hostHeader, allowedHosts) {
|
|
|
1585
1829
|
return { ok: true };
|
|
1586
1830
|
}
|
|
1587
1831
|
|
|
1832
|
+
// src/utils/auth-token.ts
|
|
1833
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1834
|
+
var BEARER_PREFIX = "Bearer ";
|
|
1835
|
+
function constantTimeEqual(a, b) {
|
|
1836
|
+
const bufA = Buffer.from(a);
|
|
1837
|
+
const bufB = Buffer.from(b);
|
|
1838
|
+
if (bufA.length !== bufB.length) return false;
|
|
1839
|
+
return timingSafeEqual(bufA, bufB);
|
|
1840
|
+
}
|
|
1841
|
+
function validateAuthToken(authorizationHeader, tokens) {
|
|
1842
|
+
if (tokens.length === 0) return { ok: true };
|
|
1843
|
+
if (!authorizationHeader || !authorizationHeader.startsWith(BEARER_PREFIX)) {
|
|
1844
|
+
return {
|
|
1845
|
+
ok: false,
|
|
1846
|
+
status: 401,
|
|
1847
|
+
message: "Missing or malformed Authorization header. Expected: Bearer <token>"
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
const presented = authorizationHeader.slice(BEARER_PREFIX.length);
|
|
1851
|
+
const matches = tokens.some((token) => constantTimeEqual(presented, token));
|
|
1852
|
+
if (!matches) {
|
|
1853
|
+
return { ok: false, status: 401, message: "Invalid bearer token" };
|
|
1854
|
+
}
|
|
1855
|
+
return { ok: true };
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1588
1858
|
// src/server.ts
|
|
1589
1859
|
var __filename = fileURLToPath(import.meta.url);
|
|
1590
1860
|
var __dirname = path.dirname(__filename);
|
|
@@ -1639,7 +1909,7 @@ See documentation for more details on configuring database connections.
|
|
|
1639
1909
|
const sources = sourceConfigsData.sources;
|
|
1640
1910
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1641
1911
|
await connectorManager.connectWithSources(sources);
|
|
1642
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1912
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-XMEJF7WW.js");
|
|
1643
1913
|
initializeToolRegistry2({
|
|
1644
1914
|
sources: sourceConfigsData.sources,
|
|
1645
1915
|
tools: sourceConfigsData.tools
|
|
@@ -1669,6 +1939,7 @@ See documentation for more details on configuring database connections.
|
|
|
1669
1939
|
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
1670
1940
|
const host = transportData.type === "http" ? resolveHost().host : null;
|
|
1671
1941
|
const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
|
|
1942
|
+
const { tokens: authTokens, source: authTokenSource } = resolveAuthTokens();
|
|
1672
1943
|
const activeModes = [];
|
|
1673
1944
|
const modeDescriptions = [];
|
|
1674
1945
|
const isDemo = isDemoMode();
|
|
@@ -1703,18 +1974,26 @@ See documentation for more details on configuring database connections.
|
|
|
1703
1974
|
}
|
|
1704
1975
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1705
1976
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1706
|
-
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1977
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1707
1978
|
res.header("Access-Control-Allow-Credentials", "true");
|
|
1708
1979
|
if (req.method === "OPTIONS") {
|
|
1709
1980
|
return res.sendStatus(200);
|
|
1710
1981
|
}
|
|
1711
1982
|
next();
|
|
1712
1983
|
});
|
|
1713
|
-
const frontendPath = path.join(__dirname, "public");
|
|
1714
|
-
app.use(express.static(frontendPath));
|
|
1715
1984
|
app.get("/healthz", (req, res) => {
|
|
1716
1985
|
res.status(200).send("OK");
|
|
1717
1986
|
});
|
|
1987
|
+
app.use((req, res, next) => {
|
|
1988
|
+
const result = validateAuthToken(req.headers.authorization, authTokens);
|
|
1989
|
+
if (!result.ok) {
|
|
1990
|
+
res.header("WWW-Authenticate", "Bearer");
|
|
1991
|
+
return res.status(result.status).json({ error: "Unauthorized", message: result.message });
|
|
1992
|
+
}
|
|
1993
|
+
next();
|
|
1994
|
+
});
|
|
1995
|
+
const frontendPath = path.join(__dirname, "public");
|
|
1996
|
+
app.use(express.static(frontendPath));
|
|
1718
1997
|
app.get("/api/sources", listSources);
|
|
1719
1998
|
app.get("/api/sources/:sourceId", getSource);
|
|
1720
1999
|
app.get("/api/requests", listRequests);
|
|
@@ -1746,6 +2025,11 @@ See documentation for more details on configuring database connections.
|
|
|
1746
2025
|
} else {
|
|
1747
2026
|
console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
|
|
1748
2027
|
}
|
|
2028
|
+
if (authTokens.length > 0) {
|
|
2029
|
+
console.error(`Auth: bearer token required (${authTokens.length} token(s) configured via ${authTokenSource})`);
|
|
2030
|
+
} else {
|
|
2031
|
+
console.error("Auth: disabled (set --auth-token or DBHUB_AUTH_TOKEN to require a bearer token)");
|
|
2032
|
+
}
|
|
1749
2033
|
if (process.env.NODE_ENV === "development") {
|
|
1750
2034
|
console.error("Development mode detected!");
|
|
1751
2035
|
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
@@ -1780,11 +2064,11 @@ See documentation for more details on configuring database connections.
|
|
|
1780
2064
|
|
|
1781
2065
|
// src/index.ts
|
|
1782
2066
|
var connectorModules = [
|
|
1783
|
-
{ load: () => import("./postgres-
|
|
1784
|
-
{ load: () => import("./sqlserver-
|
|
1785
|
-
{ load: () => import("./sqlite-
|
|
1786
|
-
{ load: () => import("./mysql-
|
|
1787
|
-
{ load: () => import("./mariadb-
|
|
2067
|
+
{ load: () => import("./postgres-6XY2GPQV.js"), name: "PostgreSQL", driver: "pg" },
|
|
2068
|
+
{ load: () => import("./sqlserver-IIGRXGYK.js"), name: "SQL Server", driver: "mssql" },
|
|
2069
|
+
{ load: () => import("./sqlite-CZE6Z36Y.js"), name: "SQLite", driver: "node:sqlite" },
|
|
2070
|
+
{ load: () => import("./mysql-RLDQI3SJ.js"), name: "MySQL", driver: "mysql2" },
|
|
2071
|
+
{ load: () => import("./mariadb-E33EXGJZ.js"), name: "MariaDB", driver: "mariadb" }
|
|
1788
2072
|
];
|
|
1789
2073
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1790
2074
|
console.error("Fatal error:", error);
|