@bytebase/dbhub 0.24.0 → 1.1.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,
@@ -15,11 +17,14 @@ import {
15
17
  resolveSourceConfigs,
16
18
  resolveTomlConfigPath,
17
19
  resolveTransport
18
- } from "./chunk-MQR6VX5F.js";
20
+ } from "./chunk-GSSLHJB6.js";
19
21
  import {
20
22
  allowedKeywords,
21
- isReadOnlySQL
22
- } from "./chunk-JO7YQPBQ.js";
23
+ getFirstKeyword,
24
+ hasEscapeHatchFunction,
25
+ isReadOnlySQL,
26
+ sqlServerDynamicSqlPattern
27
+ } from "./chunk-EPOKWSFC.js";
23
28
  import {
24
29
  loadConnectors
25
30
  } from "./chunk-WVVMH6FJ.js";
@@ -31,13 +36,14 @@ import {
31
36
  getDatabaseTypeFromDSN,
32
37
  getDefaultPortForType,
33
38
  parseConnectionInfoFromDSN,
34
- splitSQLStatements
39
+ splitSQLStatements,
40
+ stripCommentsAndStrings
35
41
  } from "./chunk-JEZZN2YZ.js";
36
42
 
37
43
  // src/server.ts
38
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
39
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
40
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
44
+ import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
45
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
46
+ import { toNodeHandler } from "@modelcontextprotocol/node";
41
47
  import express from "express";
42
48
  import http from "http";
43
49
  import path from "path";
@@ -98,6 +104,53 @@ function createToolSuccessResponse(data, meta = {}) {
98
104
  };
99
105
  }
100
106
 
107
+ // src/utils/sql-access-policy.ts
108
+ var CLASS_SEVERITY = {
109
+ read: 0,
110
+ dml: 1,
111
+ ddl: 2,
112
+ admin: 3,
113
+ unknown: 4
114
+ };
115
+ var ddlPattern = /\b(?:create|alter|drop|truncate|rename)\b/i;
116
+ var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|delayed)\s+)?into)\b/i;
117
+ var grantRevokePattern = /\b(?:grant|revoke)\b/i;
118
+ function isAdminStatement(stripped, connectorType) {
119
+ if (grantRevokePattern.test(stripped)) return true;
120
+ if (hasEscapeHatchFunction(stripped, connectorType)) return true;
121
+ return connectorType === "sqlserver" && sqlServerDynamicSqlPattern.test(stripped);
122
+ }
123
+ function policyFromReadonly(readonly) {
124
+ const write = readonly === true ? "deny" : "allow";
125
+ return { read: "allow", dml: write, ddl: write, admin: write, unknown: write };
126
+ }
127
+ function isReadOnlyPolicy(policy) {
128
+ return policy.dml === "deny" && policy.ddl === "deny" && policy.admin === "deny" && policy.unknown === "deny";
129
+ }
130
+ function classifyStatement(sql, connectorType) {
131
+ if (isReadOnlySQL(sql, connectorType)) {
132
+ return "read";
133
+ }
134
+ const stripped = stripCommentsAndStrings(sql, connectorType);
135
+ if (isAdminStatement(stripped, connectorType)) return "admin";
136
+ if (ddlPattern.test(stripped)) return "ddl";
137
+ if (dmlPattern.test(stripped)) return "dml";
138
+ return "unknown";
139
+ }
140
+ function classifySQL(sql, connectorType) {
141
+ let strictest = "read";
142
+ for (const statement of splitSQLStatements(sql, connectorType)) {
143
+ const cls = classifyStatement(statement, connectorType);
144
+ if (CLASS_SEVERITY[cls] > CLASS_SEVERITY[strictest]) {
145
+ strictest = cls;
146
+ }
147
+ }
148
+ return strictest;
149
+ }
150
+ function sqlVerdict(policy, sql, connectorType) {
151
+ return policy[classifySQL(sql, connectorType)];
152
+ }
153
+
101
154
  // src/requests/store.ts
102
155
  var RequestStore = class {
103
156
  constructor() {
@@ -149,18 +202,30 @@ var RequestStore = class {
149
202
  var requestStore = new RequestStore();
150
203
 
151
204
  // src/utils/client-identifier.ts
152
- function getClientIdentifier(extra) {
153
- const userAgent = extra?.requestInfo?.headers?.["user-agent"];
205
+ function getClientIdentifier(ctx) {
206
+ const userAgent = ctx?.http?.req?.headers.get("user-agent");
154
207
  if (userAgent) {
155
208
  return userAgent;
156
209
  }
157
210
  return "stdio";
158
211
  }
159
212
 
213
+ // src/utils/normalize-id.ts
214
+ function normalizeSourceId(id) {
215
+ return id.replace(/[^a-zA-Z0-9]/g, "_");
216
+ }
217
+
160
218
  // src/utils/tool-handler-helpers.ts
161
219
  function getEffectiveSourceId(sourceId) {
162
220
  return sourceId || "default";
163
221
  }
222
+ function resolveTrackedToolName(sourceId, baseName) {
223
+ const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
224
+ if (isSingleSource) {
225
+ return baseName;
226
+ }
227
+ return `${baseName}_${normalizeSourceId(getEffectiveSourceId(sourceId))}`;
228
+ }
164
229
  function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
165
230
  return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
166
231
  }
@@ -196,10 +261,7 @@ function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
196
261
  var executeSqlSchema = {
197
262
  sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
198
263
  };
199
- function areAllStatementsReadOnly(sql, connectorType) {
200
- const statements = splitSQLStatements(sql, connectorType);
201
- return statements.every((statement) => isReadOnlySQL(statement, connectorType));
202
- }
264
+ var executeSqlInputSchema = z.object(executeSqlSchema);
203
265
  function createExecuteSqlToolHandler(sourceId) {
204
266
  return async (args, extra) => {
205
267
  const { sql } = args;
@@ -214,14 +276,14 @@ function createExecuteSqlToolHandler(sourceId) {
214
276
  const actualSourceId = connector.getId();
215
277
  const registry = getToolRegistry();
216
278
  const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
217
- const isReadonly = toolConfig?.readonly === true;
218
- if (isReadonly && !areAllStatementsReadOnly(sql, connector.id)) {
279
+ const policy = policyFromReadonly(toolConfig?.readonly);
280
+ if (sqlVerdict(policy, sql, connector.id) === "deny") {
219
281
  errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
220
282
  success = false;
221
283
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
222
284
  }
223
285
  const executeOptions = {
224
- readonly: toolConfig?.readonly,
286
+ readonly: isReadOnlyPolicy(policy),
225
287
  maxRows: toolConfig?.max_rows
226
288
  };
227
289
  result = await connector.executeSQL(sql, executeOptions);
@@ -264,6 +326,7 @@ var searchDatabaseObjectsSchema = {
264
326
  detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
265
327
  limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
266
328
  };
329
+ var searchDatabaseObjectsInputSchema = z2.object(searchDatabaseObjectsSchema);
267
330
  function likePatternToRegex(pattern) {
268
331
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
269
332
  return new RegExp(`^${escaped}$`, "i");
@@ -738,15 +801,151 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
738
801
  };
739
802
  }
740
803
 
741
- // src/utils/tool-metadata.ts
804
+ // src/tools/explain-sql.ts
742
805
  import { z as z3 } from "zod";
806
+ var explainSqlSchema = {
807
+ sql: z3.string().describe("Single SQL statement to explain (no trailing semicolon-separated statements)")
808
+ };
809
+ var explainSqlInputSchema = z3.object(explainSqlSchema);
810
+ function buildExplainStatement(connectorType, sql) {
811
+ if (connectorType === "sqlite") {
812
+ return `EXPLAIN QUERY PLAN ${sql}`;
813
+ }
814
+ return `EXPLAIN ${sql}`;
815
+ }
816
+ function validateExplainInput(sql, connectorType) {
817
+ const statements = splitSQLStatements(sql, connectorType);
818
+ if (statements.length !== 1) {
819
+ return "explain_sql only supports a single SQL statement";
820
+ }
821
+ const firstWord = getFirstKeyword(statements[0], connectorType);
822
+ if (firstWord === "explain") {
823
+ return "explain_sql input must not itself start with EXPLAIN";
824
+ }
825
+ if (firstWord === "analyze") {
826
+ return "explain_sql does not support ANALYZE (it must never execute the statement)";
827
+ }
828
+ const cleaned = stripCommentsAndStrings(statements[0], connectorType).trim();
829
+ const leadingOptions = cleaned.match(/^\(([\s\S]*?)\)/)?.[1];
830
+ if (leadingOptions && /\banalyze\b/i.test(leadingOptions)) {
831
+ return "explain_sql does not support ANALYZE (it must never execute the statement)";
832
+ }
833
+ return null;
834
+ }
835
+ function createExplainSqlToolHandler(sourceId) {
836
+ return async (args, extra) => {
837
+ const { sql } = args;
838
+ const startTime = Date.now();
839
+ const effectiveSourceId = getEffectiveSourceId(sourceId);
840
+ let success = true;
841
+ let errorMessage;
842
+ let result;
843
+ try {
844
+ await ConnectorManager.ensureConnected(sourceId);
845
+ const connector = ConnectorManager.getCurrentConnector(sourceId);
846
+ const validationError = validateExplainInput(sql, connector.id);
847
+ if (validationError) {
848
+ success = false;
849
+ errorMessage = validationError;
850
+ return createToolErrorResponse(errorMessage, "INVALID_INPUT");
851
+ }
852
+ const explainStatement = buildExplainStatement(connector.id, sql);
853
+ result = await connector.executeSQL(explainStatement, { readonly: true });
854
+ const responseData = {
855
+ rows: result.rows,
856
+ count: result.rowCount,
857
+ source_id: effectiveSourceId,
858
+ ...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
859
+ };
860
+ return createToolSuccessResponse(responseData);
861
+ } catch (error) {
862
+ success = false;
863
+ errorMessage = error.message;
864
+ const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
865
+ if (classified) return classified;
866
+ return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
867
+ } finally {
868
+ trackToolRequest(
869
+ {
870
+ sourceId: effectiveSourceId,
871
+ toolName: resolveTrackedToolName(sourceId, "explain_sql"),
872
+ sql
873
+ },
874
+ startTime,
875
+ extra,
876
+ success,
877
+ errorMessage
878
+ );
879
+ }
880
+ };
881
+ }
743
882
 
744
- // src/utils/normalize-id.ts
745
- function normalizeSourceId(id) {
746
- return id.replace(/[^a-zA-Z0-9]/g, "_");
883
+ // src/tools/health-check.ts
884
+ import { z as z4 } from "zod";
885
+ var healthCheckSchema = {};
886
+ var healthCheckInputSchema = z4.object(healthCheckSchema);
887
+ function createHealthCheckToolHandler(sourceId) {
888
+ return async (_args, extra) => {
889
+ const startTime = Date.now();
890
+ const effectiveSourceId = getEffectiveSourceId(sourceId);
891
+ let success = true;
892
+ let errorMessage;
893
+ try {
894
+ await ConnectorManager.ensureConnected(sourceId);
895
+ const connector = ConnectorManager.getCurrentConnector(sourceId);
896
+ if (!connector.getHealthCheck) {
897
+ success = false;
898
+ errorMessage = `health_check is not supported for '${connector.id}' sources yet`;
899
+ return createToolErrorResponse(errorMessage, "UNSUPPORTED");
900
+ }
901
+ const health = await connector.getHealthCheck();
902
+ return createToolSuccessResponse({
903
+ source_id: effectiveSourceId,
904
+ ...health.connections ? {
905
+ connections: {
906
+ total: health.connections.total,
907
+ active: health.connections.active,
908
+ idle: health.connections.idle,
909
+ idle_in_transaction: health.connections.idleInTransaction,
910
+ ...health.connections.idleInTransactionAborted !== void 0 ? { idle_in_transaction_aborted: health.connections.idleInTransactionAborted } : {},
911
+ max_connections: health.connections.maxConnections,
912
+ longest_idle_in_transaction_seconds: health.connections.longestIdleInTransactionSeconds,
913
+ longest_active_query_seconds: health.connections.longestActiveQuerySeconds
914
+ }
915
+ } : {},
916
+ ...health.bufferCache ? {
917
+ buffer_cache: {
918
+ hit_ratio_pct: health.bufferCache.hitRatioPct,
919
+ blocks_hit: health.bufferCache.blocksHit,
920
+ blocks_read: health.bufferCache.blocksRead
921
+ }
922
+ } : {},
923
+ ...health.notes && health.notes.length > 0 ? { notes: health.notes } : {}
924
+ });
925
+ } catch (error) {
926
+ success = false;
927
+ errorMessage = error.message;
928
+ const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
929
+ if (classified) return classified;
930
+ return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
931
+ } finally {
932
+ trackToolRequest(
933
+ {
934
+ sourceId: effectiveSourceId,
935
+ toolName: effectiveSourceId === "default" ? "health_check" : `health_check_${effectiveSourceId}`,
936
+ sql: ""
937
+ },
938
+ startTime,
939
+ extra,
940
+ success,
941
+ errorMessage
942
+ );
943
+ }
944
+ };
747
945
  }
748
946
 
749
947
  // src/utils/tool-metadata.ts
948
+ import { z as z5 } from "zod";
750
949
  function buildSourceDescriptionPrefix(description) {
751
950
  const trimmed = description?.trim() ?? "";
752
951
  if (!trimmed) return "";
@@ -756,17 +955,17 @@ function zodToParameters(schema) {
756
955
  const parameters = [];
757
956
  for (const [key, zodType] of Object.entries(schema)) {
758
957
  const description = zodType.description || "";
759
- const required = !(zodType instanceof z3.ZodOptional);
958
+ const required = !(zodType instanceof z5.ZodOptional);
760
959
  let type = "string";
761
- if (zodType instanceof z3.ZodString) {
960
+ if (zodType instanceof z5.ZodString) {
762
961
  type = "string";
763
- } else if (zodType instanceof z3.ZodNumber) {
962
+ } else if (zodType instanceof z5.ZodNumber) {
764
963
  type = "number";
765
- } else if (zodType instanceof z3.ZodBoolean) {
964
+ } else if (zodType instanceof z5.ZodBoolean) {
766
965
  type = "boolean";
767
- } else if (zodType instanceof z3.ZodArray) {
966
+ } else if (zodType instanceof z5.ZodArray) {
768
967
  type = "array";
769
- } else if (zodType instanceof z3.ZodObject) {
968
+ } else if (zodType instanceof z5.ZodObject) {
770
969
  type = "object";
771
970
  }
772
971
  parameters.push({
@@ -778,24 +977,32 @@ function zodToParameters(schema) {
778
977
  }
779
978
  return parameters;
780
979
  }
781
- function getExecuteSqlMetadata(sourceId) {
980
+ function resolveBuiltinToolNaming(sourceId, baseName, titleLabel) {
782
981
  const sourceIds = ConnectorManager.getAvailableSourceIds();
783
982
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
784
983
  const dbType = sourceConfig.type;
785
984
  const isSingleSource = sourceIds.length === 1;
985
+ const name = isSingleSource ? baseName : `${baseName}_${normalizeSourceId(sourceId)}`;
986
+ const title = isSingleSource ? `${titleLabel} (${dbType})` : `${titleLabel} on ${sourceId} (${dbType})`;
987
+ const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
988
+ return { dbType, isSingleSource, name, title, userDescPrefix };
989
+ }
990
+ function getExecuteSqlMetadata(sourceId) {
991
+ const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
992
+ sourceId,
993
+ "execute_sql",
994
+ "Execute SQL"
995
+ );
786
996
  const registry = getToolRegistry();
787
997
  const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
788
998
  const executeOptions = {
789
999
  readonly: toolConfig?.readonly,
790
1000
  maxRows: toolConfig?.max_rows
791
1001
  };
792
- const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
793
- const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
794
- const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
795
1002
  const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
796
1003
  const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
797
1004
  const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
798
- const isReadonly = executeOptions.readonly === true;
1005
+ const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
799
1006
  const annotations = {
800
1007
  title,
801
1008
  readOnlyHint: isReadonly,
@@ -808,25 +1015,62 @@ function getExecuteSqlMetadata(sourceId) {
808
1015
  openWorldHint: false
809
1016
  };
810
1017
  return {
811
- name: toolName,
1018
+ name,
812
1019
  description,
813
1020
  schema: executeSqlSchema,
814
1021
  annotations
815
1022
  };
816
1023
  }
817
1024
  function getSearchObjectsMetadata(sourceId) {
818
- const sourceIds = ConnectorManager.getAvailableSourceIds();
819
- const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
820
- const dbType = sourceConfig.type;
821
- const isSingleSource = sourceIds.length === 1;
822
- const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
823
- const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
824
- const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
1025
+ const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
1026
+ sourceId,
1027
+ "search_objects",
1028
+ "Search Database Objects"
1029
+ );
825
1030
  const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
1031
+ return { name, description, title };
1032
+ }
1033
+ function getExplainSqlMetadata(sourceId) {
1034
+ const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
1035
+ sourceId,
1036
+ "explain_sql",
1037
+ "Explain Query Plan"
1038
+ );
1039
+ const description = isSingleSource ? `${userDescPrefix}Show the execution plan for a SQL statement on the ${dbType} database without running it. Always read-only and safe, independent of the source's read/write mode.` : `${userDescPrefix}Show the execution plan for a SQL statement on the '${sourceId}' ${dbType} database without running it. Always read-only and safe, independent of the source's read/write mode.`;
1040
+ const annotations = {
1041
+ title,
1042
+ readOnlyHint: true,
1043
+ destructiveHint: false,
1044
+ idempotentHint: true,
1045
+ openWorldHint: false
1046
+ };
1047
+ return {
1048
+ name,
1049
+ description,
1050
+ schema: explainSqlSchema,
1051
+ annotations
1052
+ };
1053
+ }
1054
+ function getHealthCheckMetadata(sourceId) {
1055
+ const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
1056
+ sourceId,
1057
+ "health_check",
1058
+ "Health Check"
1059
+ );
1060
+ const description = isSingleSource ? `${userDescPrefix}Report operational health metrics (connection pool state, buffer cache hit ratio) for the ${dbType} database. Read-only, independent of the source's read/write mode.` : `${userDescPrefix}Report operational health metrics (connection pool state, buffer cache hit ratio) for the '${sourceId}' ${dbType} database. Read-only, independent of the source's read/write mode.`;
1061
+ const annotations = {
1062
+ title,
1063
+ readOnlyHint: true,
1064
+ destructiveHint: false,
1065
+ idempotentHint: false,
1066
+ // Metrics change from call to call, unlike explain_sql's plan output
1067
+ openWorldHint: false
1068
+ };
826
1069
  return {
827
- name: toolName,
1070
+ name,
828
1071
  description,
829
- title
1072
+ schema: healthCheckSchema,
1073
+ annotations
830
1074
  };
831
1075
  }
832
1076
  function customParamsToToolParams(params) {
@@ -900,6 +1144,16 @@ function buildSearchObjectsTool(sourceId) {
900
1144
  // search_objects is always readonly
901
1145
  };
902
1146
  }
1147
+ function buildExplainSqlTool(sourceId) {
1148
+ const explainMetadata = getExplainSqlMetadata(sourceId);
1149
+ return {
1150
+ name: explainMetadata.name,
1151
+ description: explainMetadata.description,
1152
+ parameters: zodToParameters(explainMetadata.schema),
1153
+ readonly: true
1154
+ // explain_sql is always readonly
1155
+ };
1156
+ }
903
1157
  function buildCustomTool(toolConfig) {
904
1158
  return {
905
1159
  name: toolConfig.name,
@@ -918,6 +1172,8 @@ function getToolsForSource(sourceId) {
918
1172
  return buildExecuteSqlTool(sourceId, toolConfig);
919
1173
  } else if (toolConfig.name === "search_objects") {
920
1174
  return buildSearchObjectsTool(sourceId);
1175
+ } else if (toolConfig.name === "explain_sql") {
1176
+ return buildExplainSqlTool(sourceId);
921
1177
  } else {
922
1178
  return buildCustomTool(toolConfig);
923
1179
  }
@@ -925,7 +1181,7 @@ function getToolsForSource(sourceId) {
925
1181
  }
926
1182
 
927
1183
  // src/tools/custom-tool-handler.ts
928
- import { z as z4 } from "zod";
1184
+ import { z as z6 } from "zod";
929
1185
  function buildZodSchemaFromParameters(parameters) {
930
1186
  if (!parameters || parameters.length === 0) {
931
1187
  return {};
@@ -935,26 +1191,26 @@ function buildZodSchemaFromParameters(parameters) {
935
1191
  let fieldSchema;
936
1192
  switch (param.type) {
937
1193
  case "string":
938
- fieldSchema = z4.string().describe(param.description);
1194
+ fieldSchema = z6.string().describe(param.description);
939
1195
  break;
940
1196
  case "integer":
941
- fieldSchema = z4.number().int().describe(param.description);
1197
+ fieldSchema = z6.number().int().describe(param.description);
942
1198
  break;
943
1199
  case "float":
944
- fieldSchema = z4.number().describe(param.description);
1200
+ fieldSchema = z6.number().describe(param.description);
945
1201
  break;
946
1202
  case "boolean":
947
- fieldSchema = z4.boolean().describe(param.description);
1203
+ fieldSchema = z6.boolean().describe(param.description);
948
1204
  break;
949
1205
  case "array":
950
- fieldSchema = z4.array(z4.unknown()).describe(param.description);
1206
+ fieldSchema = z6.array(z6.unknown()).describe(param.description);
951
1207
  break;
952
1208
  default:
953
1209
  throw new Error(`Unsupported parameter type: ${param.type}`);
954
1210
  }
955
1211
  if (param.allowed_values && param.allowed_values.length > 0) {
956
1212
  if (param.type === "string") {
957
- fieldSchema = z4.enum(param.allowed_values).describe(param.description);
1213
+ fieldSchema = z6.enum(param.allowed_values).describe(param.description);
958
1214
  } else {
959
1215
  fieldSchema = fieldSchema.refine(
960
1216
  (val) => param.allowed_values.includes(val),
@@ -971,9 +1227,17 @@ function buildZodSchemaFromParameters(parameters) {
971
1227
  }
972
1228
  return schemaShape;
973
1229
  }
1230
+ var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
1231
+ function getCustomToolInputSchema(toolConfig) {
1232
+ let schema = customToolInputSchemas.get(toolConfig);
1233
+ if (!schema) {
1234
+ schema = z6.object(buildZodSchemaFromParameters(toolConfig.parameters));
1235
+ customToolInputSchemas.set(toolConfig, schema);
1236
+ }
1237
+ return schema;
1238
+ }
974
1239
  function createCustomToolHandler(toolConfig) {
975
- const zodSchemaShape = buildZodSchemaFromParameters(toolConfig.parameters);
976
- const zodSchema = z4.object(zodSchemaShape);
1240
+ const zodSchema = getCustomToolInputSchema(toolConfig);
977
1241
  return async (args, extra) => {
978
1242
  const startTime = Date.now();
979
1243
  let success = true;
@@ -983,12 +1247,12 @@ function createCustomToolHandler(toolConfig) {
983
1247
  const validatedArgs = zodSchema.parse(args);
984
1248
  await ConnectorManager.ensureConnected(toolConfig.source);
985
1249
  const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
1250
+ const policy = policyFromReadonly(toolConfig.readonly);
986
1251
  const executeOptions = {
987
- readonly: toolConfig.readonly,
1252
+ readonly: isReadOnlyPolicy(policy),
988
1253
  maxRows: toolConfig.max_rows
989
1254
  };
990
- const isReadonly = executeOptions.readonly === true;
991
- if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
1255
+ if (sqlVerdict(policy, toolConfig.statement, connector.id) === "deny") {
992
1256
  errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
993
1257
  success = false;
994
1258
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
@@ -1013,7 +1277,7 @@ function createCustomToolHandler(toolConfig) {
1013
1277
  errorMessage = error.message;
1014
1278
  const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
1015
1279
  if (classified) return classified;
1016
- if (error instanceof z4.ZodError) {
1280
+ if (error instanceof z6.ZodError) {
1017
1281
  const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1018
1282
  errorMessage = `Parameter validation failed: ${issues}`;
1019
1283
  } else {
@@ -1053,6 +1317,10 @@ function registerTools(server) {
1053
1317
  registerExecuteSqlTool(server, sourceId);
1054
1318
  } else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
1055
1319
  registerSearchObjectsTool(server, sourceId);
1320
+ } else if (toolConfig.name === BUILTIN_TOOL_EXPLAIN_SQL) {
1321
+ registerExplainSqlTool(server, sourceId);
1322
+ } else if (toolConfig.name === BUILTIN_TOOL_HEALTH_CHECK) {
1323
+ registerHealthCheckTool(server, sourceId);
1056
1324
  } else {
1057
1325
  registerCustomTool(server, sourceId, toolConfig);
1058
1326
  }
@@ -1065,7 +1333,7 @@ function registerExecuteSqlTool(server, sourceId) {
1065
1333
  metadata.name,
1066
1334
  {
1067
1335
  description: metadata.description,
1068
- inputSchema: metadata.schema,
1336
+ inputSchema: executeSqlInputSchema,
1069
1337
  annotations: metadata.annotations
1070
1338
  },
1071
1339
  createExecuteSqlToolHandler(sourceId)
@@ -1077,7 +1345,7 @@ function registerSearchObjectsTool(server, sourceId) {
1077
1345
  metadata.name,
1078
1346
  {
1079
1347
  description: metadata.description,
1080
- inputSchema: searchDatabaseObjectsSchema,
1348
+ inputSchema: searchDatabaseObjectsInputSchema,
1081
1349
  annotations: {
1082
1350
  title: metadata.title,
1083
1351
  readOnlyHint: true,
@@ -1089,16 +1357,39 @@ function registerSearchObjectsTool(server, sourceId) {
1089
1357
  createSearchDatabaseObjectsToolHandler(sourceId)
1090
1358
  );
1091
1359
  }
1360
+ function registerExplainSqlTool(server, sourceId) {
1361
+ const metadata = getExplainSqlMetadata(sourceId);
1362
+ server.registerTool(
1363
+ metadata.name,
1364
+ {
1365
+ description: metadata.description,
1366
+ inputSchema: explainSqlInputSchema,
1367
+ annotations: metadata.annotations
1368
+ },
1369
+ createExplainSqlToolHandler(sourceId)
1370
+ );
1371
+ }
1372
+ function registerHealthCheckTool(server, sourceId) {
1373
+ const metadata = getHealthCheckMetadata(sourceId);
1374
+ server.registerTool(
1375
+ metadata.name,
1376
+ {
1377
+ description: metadata.description,
1378
+ inputSchema: healthCheckInputSchema,
1379
+ annotations: metadata.annotations
1380
+ },
1381
+ createHealthCheckToolHandler(sourceId)
1382
+ );
1383
+ }
1092
1384
  function registerCustomTool(server, sourceId, toolConfig) {
1093
1385
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
1094
1386
  const dbType = sourceConfig.type;
1095
- const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
1096
- const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
1387
+ const isReadOnly = classifySQL(toolConfig.statement, dbType) === "read";
1097
1388
  server.registerTool(
1098
1389
  toolConfig.name,
1099
1390
  {
1100
1391
  description: toolConfig.description,
1101
- inputSchema: zodSchema,
1392
+ inputSchema: getCustomToolInputSchema(toolConfig),
1102
1393
  annotations: {
1103
1394
  title: `${toolConfig.name} (${dbType})`,
1104
1395
  readOnlyHint: isReadOnly,
@@ -1538,6 +1829,8 @@ var packageJsonPath = path.join(__dirname, "..", "package.json");
1538
1829
  var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1539
1830
  var SERVER_NAME = "DBHub MCP Server";
1540
1831
  var SERVER_VERSION = packageJson.version;
1832
+ var TOOLS_LIST_CACHE_TTL_MS = 3e5;
1833
+ var logMcpError = (scope) => (error) => console.error(`MCP ${scope} error:`, error);
1541
1834
  function generateBanner(version, modes = []) {
1542
1835
  const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
1543
1836
  return `
@@ -1583,7 +1876,7 @@ See documentation for more details on configuring database connections.
1583
1876
  const sources = sourceConfigsData.sources;
1584
1877
  console.error(`Configuration source: ${sourceConfigsData.source}`);
1585
1878
  await connectorManager.connectWithSources(sources);
1586
- const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-ZU6ZFNQ2.js");
1879
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-JYRRNC3E.js");
1587
1880
  initializeToolRegistry2({
1588
1881
  sources: sourceConfigsData.sources,
1589
1882
  tools: sourceConfigsData.tools
@@ -1594,10 +1887,18 @@ See documentation for more details on configuring database connections.
1594
1887
  initialTools: sourceConfigsData.tools
1595
1888
  });
1596
1889
  const createServer = () => {
1597
- const server = new McpServer({
1598
- name: SERVER_NAME,
1599
- version: SERVER_VERSION
1600
- });
1890
+ const server = new McpServer(
1891
+ {
1892
+ name: SERVER_NAME,
1893
+ version: SERVER_VERSION
1894
+ },
1895
+ {
1896
+ // `private` keeps caching client-side (no shared/proxy caches).
1897
+ cacheHints: {
1898
+ "tools/list": { ttlMs: TOOLS_LIST_CACHE_TTL_MS, cacheScope: "private" }
1899
+ }
1900
+ }
1901
+ );
1601
1902
  registerTools(server);
1602
1903
  return server;
1603
1904
  };
@@ -1639,7 +1940,7 @@ See documentation for more details on configuring database connections.
1639
1940
  }
1640
1941
  res.header("Access-Control-Allow-Origin", origin || "http://localhost");
1641
1942
  res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1642
- res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
1943
+ res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
1643
1944
  res.header("Access-Control-Allow-Credentials", "true");
1644
1945
  if (req.method === "OPTIONS") {
1645
1946
  return res.sendStatus(200);
@@ -1654,29 +1955,10 @@ See documentation for more details on configuring database connections.
1654
1955
  app.get("/api/sources", listSources);
1655
1956
  app.get("/api/sources/:sourceId", getSource);
1656
1957
  app.get("/api/requests", listRequests);
1657
- app.get("/mcp", (req, res) => {
1658
- res.status(405).json({
1659
- error: "Method Not Allowed",
1660
- message: "SSE streaming is not supported in stateless mode. Use POST requests with JSON responses."
1661
- });
1662
- });
1663
- app.post("/mcp", async (req, res) => {
1664
- try {
1665
- const transport = new StreamableHTTPServerTransport({
1666
- sessionIdGenerator: void 0,
1667
- // Disable session management for stateless mode
1668
- enableJsonResponse: true
1669
- // Use JSON responses (SSE not supported in stateless mode)
1670
- });
1671
- const server = createServer();
1672
- await server.connect(transport);
1673
- await transport.handleRequest(req, res, req.body);
1674
- } catch (error) {
1675
- console.error("Error handling request:", error);
1676
- if (!res.headersSent) {
1677
- res.status(500).json({ error: "Internal server error" });
1678
- }
1679
- }
1958
+ const mcpHandler = createMcpHandler(createServer, { onerror: logMcpError("handler") });
1959
+ const mcpNodeHandler = toNodeHandler(mcpHandler, { onerror: logMcpError("adapter") });
1960
+ app.all("/mcp", (req, res) => {
1961
+ void mcpNodeHandler(req, res, req.body);
1680
1962
  });
1681
1963
  if (process.env.NODE_ENV !== "development") {
1682
1964
  app.get("*", (req, res) => {
@@ -1712,16 +1994,14 @@ See documentation for more details on configuring database connections.
1712
1994
  console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
1713
1995
  });
1714
1996
  } else {
1715
- const server = createServer();
1716
- const transport = new StdioServerTransport();
1717
- await server.connect(transport);
1997
+ const stdioHandle = serveStdio(createServer, { onerror: logMcpError("stdio") });
1718
1998
  console.error("MCP server running on stdio");
1719
1999
  let isShuttingDown = false;
1720
2000
  const shutdown = async () => {
1721
2001
  if (isShuttingDown) return;
1722
2002
  isShuttingDown = true;
1723
2003
  console.error("Shutting down...");
1724
- await transport.close();
2004
+ await stdioHandle.close();
1725
2005
  await connectorManager.disconnect();
1726
2006
  process.exit(0);
1727
2007
  };
@@ -1737,11 +2017,11 @@ See documentation for more details on configuring database connections.
1737
2017
 
1738
2018
  // src/index.ts
1739
2019
  var connectorModules = [
1740
- { load: () => import("./postgres-ZQFXCBYA.js"), name: "PostgreSQL", driver: "pg" },
1741
- { load: () => import("./sqlserver-LHWZ7WOP.js"), name: "SQL Server", driver: "mssql" },
1742
- { load: () => import("./sqlite-JH5CNV5E.js"), name: "SQLite", driver: "node:sqlite" },
1743
- { load: () => import("./mysql-MHIZK2UY.js"), name: "MySQL", driver: "mysql2" },
1744
- { load: () => import("./mariadb-VZMU7LYA.js"), name: "MariaDB", driver: "mariadb" }
2020
+ { load: () => import("./postgres-5ZOFYV6X.js"), name: "PostgreSQL", driver: "pg" },
2021
+ { load: () => import("./sqlserver-PSAZRH7G.js"), name: "SQL Server", driver: "mssql" },
2022
+ { load: () => import("./sqlite-5M7SDAQZ.js"), name: "SQLite", driver: "node:sqlite" },
2023
+ { load: () => import("./mysql-265M3KBD.js"), name: "MySQL", driver: "mysql2" },
2024
+ { load: () => import("./mariadb-DUDFBY5W.js"), name: "MariaDB", driver: "mariadb" }
1745
2025
  ];
1746
2026
  loadConnectors(connectorModules).then(() => main()).catch((error) => {
1747
2027
  console.error("Fatal error:", error);