@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/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-MQR6VX5F.js";
21
+ } from "./chunk-CUBIR72I.js";
19
22
  import {
20
23
  allowedKeywords,
24
+ getFirstKeyword,
25
+ hasEscapeHatchFunction,
21
26
  isReadOnlySQL,
22
- sqlServerDynamicSqlPattern,
23
- sqlServerPassThroughPattern
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-JEZZN2YZ.js";
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
- return connectorType === "sqlserver" && (sqlServerDynamicSqlPattern.test(stripped) || sqlServerPassThroughPattern.test(stripped));
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
- rows: result.rows,
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
- if (result.rows && result.rows.length > 0) {
336
- return Number(result.rows[0].count || result.rows[0].COUNT || 0);
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/utils/tool-metadata.ts
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/utils/normalize-id.ts
792
- function normalizeSourceId(id) {
793
- return id.replace(/[^a-zA-Z0-9]/g, "_");
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 z3.ZodOptional);
966
+ const required = !(zodType instanceof z5.ZodOptional);
807
967
  let type = "string";
808
- if (zodType instanceof z3.ZodString) {
968
+ if (zodType instanceof z5.ZodString) {
809
969
  type = "string";
810
- } else if (zodType instanceof z3.ZodNumber) {
970
+ } else if (zodType instanceof z5.ZodNumber) {
811
971
  type = "number";
812
- } else if (zodType instanceof z3.ZodBoolean) {
972
+ } else if (zodType instanceof z5.ZodBoolean) {
813
973
  type = "boolean";
814
- } else if (zodType instanceof z3.ZodArray) {
974
+ } else if (zodType instanceof z5.ZodArray) {
815
975
  type = "array";
816
- } else if (zodType instanceof z3.ZodObject) {
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 getExecuteSqlMetadata(sourceId) {
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: toolName,
1026
+ name,
859
1027
  description,
860
1028
  schema: executeSqlSchema,
861
1029
  annotations
862
1030
  };
863
1031
  }
864
1032
  function getSearchObjectsMetadata(sourceId) {
865
- const sourceIds = ConnectorManager.getAvailableSourceIds();
866
- const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
867
- const dbType = sourceConfig.type;
868
- const isSingleSource = sourceIds.length === 1;
869
- const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
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: toolName,
1078
+ name,
875
1079
  description,
876
- title
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: schema, table, view, column, procedure, function, index"
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). Default: %"
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 (default: 100, max: 1000)"
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 z4 } from "zod";
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 = z4.string().describe(param.description);
1202
+ fieldSchema = z6.string().describe(param.description);
986
1203
  break;
987
1204
  case "integer":
988
- fieldSchema = z4.number().int().describe(param.description);
1205
+ fieldSchema = z6.number().int().describe(param.description);
989
1206
  break;
990
1207
  case "float":
991
- fieldSchema = z4.number().describe(param.description);
1208
+ fieldSchema = z6.number().describe(param.description);
992
1209
  break;
993
1210
  case "boolean":
994
- fieldSchema = z4.boolean().describe(param.description);
1211
+ fieldSchema = z6.boolean().describe(param.description);
995
1212
  break;
996
1213
  case "array":
997
- fieldSchema = z4.array(z4.unknown()).describe(param.description);
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 = z4.enum(param.allowed_values).describe(param.description);
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 = z4.object(buildZodSchemaFromParameters(toolConfig.parameters));
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
- rows: result.rows,
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 z4.ZodError) {
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-ZU6ZFNQ2.js");
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-YAYHDXVD.js"), name: "PostgreSQL", driver: "pg" },
1784
- { load: () => import("./sqlserver-COKW7RIK.js"), name: "SQL Server", driver: "mssql" },
1785
- { load: () => import("./sqlite-5M7SDAQZ.js"), name: "SQLite", driver: "node:sqlite" },
1786
- { load: () => import("./mysql-CDUKJMNN.js"), name: "MySQL", driver: "mysql2" },
1787
- { load: () => import("./mariadb-72C2BNCG.js"), name: "MariaDB", driver: "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);