@bytebase/dbhub 0.22.3 → 0.22.4

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.
@@ -1094,6 +1094,92 @@ function validateToolsConfig(tools, sources, configPath) {
1094
1094
  }
1095
1095
  }
1096
1096
  }
1097
+ function getRawDSNQueryParam(dsn, key) {
1098
+ const queryStart = dsn.indexOf("?");
1099
+ if (queryStart === -1) {
1100
+ return null;
1101
+ }
1102
+ for (const pair of dsn.substring(queryStart + 1).split("&")) {
1103
+ if (pair === "") {
1104
+ continue;
1105
+ }
1106
+ const eq = pair.indexOf("=");
1107
+ const rawKey = eq === -1 ? pair : pair.substring(0, eq);
1108
+ if (rawKey === key) {
1109
+ const rawValue = eq === -1 ? "" : pair.substring(eq + 1);
1110
+ try {
1111
+ return decodeURIComponent(rawValue);
1112
+ } catch {
1113
+ return rawValue;
1114
+ }
1115
+ }
1116
+ }
1117
+ return null;
1118
+ }
1119
+ function validateDSNFieldConflicts(source, configPath) {
1120
+ const conflict = (field, fieldValue, dsnValue) => {
1121
+ throw new Error(
1122
+ `Configuration file ${configPath}: source '${source.id}' has conflicting ${field}: the DSN specifies '${dsnValue}' but the ${field} field is '${fieldValue}'. Set ${field} in only one place, or make the two values match.`
1123
+ );
1124
+ };
1125
+ const info = parseConnectionInfoFromDSN(source.dsn);
1126
+ if (source.type && info?.type && source.type !== info.type) {
1127
+ conflict("type", source.type, info.type);
1128
+ }
1129
+ if (info?.type === "sqlite") {
1130
+ return;
1131
+ }
1132
+ let url;
1133
+ try {
1134
+ url = new SafeURL(source.dsn);
1135
+ } catch {
1136
+ return;
1137
+ }
1138
+ if (info) {
1139
+ if (source.host && info.host && source.host.toLowerCase() !== info.host.toLowerCase()) {
1140
+ conflict("host", source.host, info.host);
1141
+ }
1142
+ if (source.port !== void 0 && info.port !== void 0 && source.port !== info.port) {
1143
+ conflict("port", String(source.port), String(info.port));
1144
+ }
1145
+ if (source.database && info.database && source.database !== info.database) {
1146
+ conflict("database", source.database, info.database);
1147
+ }
1148
+ if (source.user && info.user && source.user !== info.user) {
1149
+ conflict("user", source.user, info.user);
1150
+ }
1151
+ }
1152
+ if (source.password && source.password !== url.password) {
1153
+ if (!url.password) {
1154
+ throw new Error(
1155
+ `Configuration file ${configPath}: source '${source.id}' has a 'password' field but the DSN has no password. The field is ignored at connection time \u2014 add the password to the DSN, or use individual connection parameters instead of a DSN.`
1156
+ );
1157
+ }
1158
+ throw new Error(
1159
+ `Configuration file ${configPath}: source '${source.id}' has a 'password' field that conflicts with the password in the DSN. Set the password in only one place.`
1160
+ );
1161
+ }
1162
+ const dsnSslmode = getRawDSNQueryParam(source.dsn, "sslmode");
1163
+ if (source.sslmode && dsnSslmode !== null && dsnSslmode !== source.sslmode) {
1164
+ conflict("sslmode", source.sslmode, dsnSslmode);
1165
+ }
1166
+ const dsnSslrootcert = getRawDSNQueryParam(source.dsn, "sslrootcert");
1167
+ if (source.sslrootcert && dsnSslrootcert !== null && expandHomeDir(source.sslrootcert) !== expandHomeDir(dsnSslrootcert)) {
1168
+ conflict("sslrootcert", expandHomeDir(source.sslrootcert), expandHomeDir(dsnSslrootcert));
1169
+ }
1170
+ const dsnInstanceName = getRawDSNQueryParam(source.dsn, "instanceName");
1171
+ if (source.instanceName && dsnInstanceName !== null && dsnInstanceName !== source.instanceName) {
1172
+ conflict("instanceName", source.instanceName, dsnInstanceName);
1173
+ }
1174
+ const dsnAuthentication = getRawDSNQueryParam(source.dsn, "authentication");
1175
+ if (source.authentication && dsnAuthentication !== null && dsnAuthentication !== source.authentication) {
1176
+ conflict("authentication", source.authentication, dsnAuthentication);
1177
+ }
1178
+ const dsnDomain = getRawDSNQueryParam(source.dsn, "domain");
1179
+ if (source.domain && dsnDomain !== null && dsnDomain !== source.domain) {
1180
+ conflict("domain", source.domain, dsnDomain);
1181
+ }
1182
+ }
1097
1183
  function validateSourceConfig(source, configPath) {
1098
1184
  const hasConnectionParams = source.type && (source.type === "sqlite" ? source.database : source.host);
1099
1185
  if (!source.dsn && !hasConnectionParams) {
@@ -1176,6 +1262,9 @@ function validateSourceConfig(source, configPath) {
1176
1262
  );
1177
1263
  }
1178
1264
  }
1265
+ if (source.dsn) {
1266
+ validateDSNFieldConflicts(source, configPath);
1267
+ }
1179
1268
  if (source.sslrootcert !== void 0) {
1180
1269
  if (source.sslmode !== "verify-ca" && source.sslmode !== "verify-full") {
1181
1270
  throw new Error(
@@ -1350,9 +1439,51 @@ function expandHomeDir(filePath) {
1350
1439
  }
1351
1440
  return filePath;
1352
1441
  }
1442
+ function mergeSourceFieldsIntoDSN(dsn, source) {
1443
+ if (source.type === "sqlite") {
1444
+ return dsn;
1445
+ }
1446
+ try {
1447
+ new SafeURL(dsn);
1448
+ } catch {
1449
+ return dsn;
1450
+ }
1451
+ const hasParam = (key) => getRawDSNQueryParam(dsn, key) !== null;
1452
+ const additions = [];
1453
+ if (source.type === "sqlserver") {
1454
+ if (source.instanceName && !hasParam("instanceName")) {
1455
+ additions.push(`instanceName=${encodeURIComponent(source.instanceName)}`);
1456
+ }
1457
+ if (source.authentication && !hasParam("authentication")) {
1458
+ additions.push(`authentication=${encodeURIComponent(source.authentication)}`);
1459
+ }
1460
+ if (source.domain && !hasParam("domain")) {
1461
+ additions.push(`domain=${encodeURIComponent(source.domain)}`);
1462
+ }
1463
+ }
1464
+ if (source.sslmode && !hasParam("sslmode")) {
1465
+ additions.push(`sslmode=${source.sslmode}`);
1466
+ }
1467
+ if (source.sslrootcert && source.type === "postgres" && (source.sslmode === "verify-ca" || source.sslmode === "verify-full") && !hasParam("sslrootcert")) {
1468
+ const expandedCertPath = expandHomeDir(source.sslrootcert);
1469
+ additions.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
1470
+ }
1471
+ if (additions.length === 0) {
1472
+ return dsn;
1473
+ }
1474
+ let separator;
1475
+ if (!dsn.includes("?")) {
1476
+ separator = "?";
1477
+ } else if (dsn.endsWith("?") || dsn.endsWith("&")) {
1478
+ separator = "";
1479
+ } else {
1480
+ separator = "&";
1481
+ }
1482
+ return `${dsn}${separator}${additions.join("&")}`;
1483
+ }
1353
1484
  function buildDSNFromSource(source) {
1354
1485
  if (source.dsn) {
1355
- return source.dsn;
1486
+ return mergeSourceFieldsIntoDSN(source.dsn, source);
1356
1487
  }
1357
1488
  if (!source.type) {
1358
1489
  throw new Error(
@@ -1434,6 +1565,53 @@ async function generateRdsAuthToken(params) {
1434
1565
  return signer.getAuthToken();
1435
1566
  }
1436
1567
 
1568
+ // src/utils/error-classifier.ts
1569
+ var TUNNEL_ERROR_MARKER = "__dbhubSSHTunnelError";
1570
+ var NETWORK_CODES = /* @__PURE__ */ new Set([
1571
+ "ECONNREFUSED",
1572
+ "ETIMEDOUT",
1573
+ "ENOTFOUND",
1574
+ "EHOSTUNREACH",
1575
+ "ENETUNREACH",
1576
+ "ECONNRESET"
1577
+ ]);
1578
+ var AUTH_CODES = {
1579
+ postgres: ["28P01", "28000"],
1580
+ mysql: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
1581
+ mariadb: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
1582
+ sqlserver: ["ELOGIN"],
1583
+ sqlite: []
1584
+ // no network/auth layer
1585
+ };
1586
+ function unreachableMessage(sourceId) {
1587
+ return `Source '${sourceId}' is unreachable. Verify the database is running and reachable (host, port, network), then retry.`;
1588
+ }
1589
+ function authMessage(sourceId) {
1590
+ return `Authentication failed for source '${sourceId}'. Verify the credentials/access for this source are valid, then retry.`;
1591
+ }
1592
+ function tunnelMessage(sourceId) {
1593
+ return `SSH tunnel for source '${sourceId}' failed to establish. Verify SSH host/credentials and bastion reachability, then retry.`;
1594
+ }
1595
+ function classifyConnectionError(error, connectorType, sourceId) {
1596
+ if (!error || typeof error !== "object") {
1597
+ return null;
1598
+ }
1599
+ const err = error;
1600
+ if (err[TUNNEL_ERROR_MARKER] === true) {
1601
+ return { code: "TUNNEL_FAILED", message: tunnelMessage(sourceId) };
1602
+ }
1603
+ const code = err.code;
1604
+ if (typeof code === "string" && NETWORK_CODES.has(code)) {
1605
+ return { code: "SOURCE_UNREACHABLE", message: unreachableMessage(sourceId) };
1606
+ }
1607
+ const authCodes = AUTH_CODES[connectorType];
1608
+ const errno = err.errno;
1609
+ if (typeof code === "string" && authCodes.includes(code) || typeof errno === "number" && authCodes.includes(errno)) {
1610
+ return { code: "AUTH_FAILED", message: authMessage(sourceId) };
1611
+ }
1612
+ return null;
1613
+ }
1614
+
1437
1615
  // src/connectors/manager.ts
1438
1616
  var managerInstance = null;
1439
1617
  var AWS_IAM_TOKEN_REFRESH_MS = 14 * 60 * 1e3;
@@ -1574,10 +1752,18 @@ var ConnectorManager = class {
1574
1752
  const targetHost = url.hostname;
1575
1753
  const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
1576
1754
  const tunnel = new SSHTunnel();
1577
- const tunnelInfo = await tunnel.establish(sshConfig, {
1578
- targetHost,
1579
- targetPort
1580
- });
1755
+ let tunnelInfo;
1756
+ try {
1757
+ tunnelInfo = await tunnel.establish(sshConfig, {
1758
+ targetHost,
1759
+ targetPort
1760
+ });
1761
+ } catch (error) {
1762
+ if (error && typeof error === "object") {
1763
+ error[TUNNEL_ERROR_MARKER] = true;
1764
+ }
1765
+ throw error;
1766
+ }
1581
1767
  url.hostname = "127.0.0.1";
1582
1768
  url.port = tunnelInfo.localPort.toString();
1583
1769
  actualDSN = url.toString();
@@ -2190,6 +2376,7 @@ export {
2190
2376
  BUILTIN_TOOL_SEARCH_OBJECTS,
2191
2377
  loadTomlConfig,
2192
2378
  resolveTomlConfigPath,
2379
+ classifyConnectionError,
2193
2380
  ConnectorManager,
2194
2381
  mapArgumentsToArray,
2195
2382
  ToolRegistry,
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  BUILTIN_TOOL_EXECUTE_SQL,
4
4
  BUILTIN_TOOL_SEARCH_OBJECTS,
5
5
  ConnectorManager,
6
+ classifyConnectionError,
6
7
  getToolRegistry,
7
8
  initializeToolRegistry,
8
9
  isDemoMode,
@@ -13,7 +14,7 @@ import {
13
14
  resolveSourceConfigs,
14
15
  resolveTomlConfigPath,
15
16
  resolveTransport
16
- } from "./chunk-MEBWJMLY.js";
17
+ } from "./chunk-DN45LGY4.js";
17
18
  import {
18
19
  loadConnectors
19
20
  } from "./chunk-WVVMH6FJ.js";
@@ -248,6 +249,20 @@ function trackToolRequest(metadata, startTime, extra, success, error) {
248
249
  error
249
250
  });
250
251
  }
252
+ function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
253
+ let connectorType;
254
+ try {
255
+ connectorType = ConnectorManager.getSourceConfig(rawSourceId)?.type;
256
+ } catch {
257
+ return null;
258
+ }
259
+ if (!connectorType) return null;
260
+ const classified = classifyConnectionError(error, connectorType, displaySourceId);
261
+ if (!classified) return null;
262
+ return createToolErrorResponse(classified.message, classified.code, {
263
+ source_id: displaySourceId
264
+ });
265
+ }
251
266
 
252
267
  // src/tools/execute-sql.ts
253
268
  var executeSqlSchema = {
@@ -292,6 +307,8 @@ function createExecuteSqlToolHandler(sourceId) {
292
307
  } catch (error) {
293
308
  success = false;
294
309
  errorMessage = error.message;
310
+ const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
311
+ if (classified) return classified;
295
312
  return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
296
313
  } finally {
297
314
  trackToolRequest(
@@ -313,11 +330,11 @@ function createExecuteSqlToolHandler(sourceId) {
313
330
  import { z as z2 } from "zod";
314
331
  var searchDatabaseObjectsSchema = {
315
332
  object_type: z2.enum(["schema", "table", "view", "column", "procedure", "function", "index"]).describe("Object type to search"),
316
- pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char). Default: %"),
333
+ pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char)"),
317
334
  schema: z2.string().optional().describe("Filter to schema"),
318
335
  table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
319
336
  detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
320
- limit: z2.number().int().positive().max(1e3).default(100).describe("Max results (default: 100, max: 1000)")
337
+ limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
321
338
  };
322
339
  function likePatternToRegex(pattern) {
323
340
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
@@ -771,6 +788,8 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
771
788
  } catch (error) {
772
789
  success = false;
773
790
  errorMessage = error.message;
791
+ const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
792
+ if (classified) return classified;
774
793
  return createToolErrorResponse(
775
794
  `Error searching database objects: ${errorMessage}`,
776
795
  "SEARCH_ERROR"
@@ -875,7 +894,7 @@ function getSearchObjectsMetadata(sourceId) {
875
894
  const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
876
895
  const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
877
896
  const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
878
- const description = isSingleSource ? `${userDescPrefix}Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the ${dbType} database` : `${userDescPrefix}Search and list database objects (schemas, tables, columns, procedures, functions, indexes) on the '${sourceId}' ${dbType} database`;
897
+ const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
879
898
  return {
880
899
  name: toolName,
881
900
  description,
@@ -916,7 +935,7 @@ function buildSearchObjectsTool(sourceId) {
916
935
  name: "object_type",
917
936
  type: "string",
918
937
  required: true,
919
- description: "Object type to search"
938
+ description: "Object type to search: schema, table, view, column, procedure, function, index"
920
939
  },
921
940
  {
922
941
  name: "pattern",
@@ -1064,6 +1083,8 @@ function createCustomToolHandler(toolConfig) {
1064
1083
  } catch (error) {
1065
1084
  success = false;
1066
1085
  errorMessage = error.message;
1086
+ const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
1087
+ if (classified) return classified;
1067
1088
  if (error instanceof z4.ZodError) {
1068
1089
  const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1069
1090
  errorMessage = `Parameter validation failed: ${issues}`;
@@ -1557,7 +1578,7 @@ See documentation for more details on configuring database connections.
1557
1578
  const sources = sourceConfigsData.sources;
1558
1579
  console.error(`Configuration source: ${sourceConfigsData.source}`);
1559
1580
  await connectorManager.connectWithSources(sources);
1560
- const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-FZPB5LRL.js");
1581
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-OF5GPCBK.js");
1561
1582
  initializeToolRegistry2({
1562
1583
  sources: sourceConfigsData.sources,
1563
1584
  tools: sourceConfigsData.tools
@@ -2,7 +2,7 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-MEBWJMLY.js";
5
+ } from "./chunk-DN45LGY4.js";
6
6
  import "./chunk-WVVMH6FJ.js";
7
7
  import "./chunk-C7WEAPX4.js";
8
8
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.22.3",
3
+ "version": "0.22.4",
4
4
  "mcpName": "io.github.bytebase/dbhub",
5
5
  "description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
6
6
  "repository": {