@bytebase/dbhub 1.1.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  > [!NOTE]
2
- > Brought to you by [Bytebase](https://www.bytebase.com/), open-source database DevSecOps platform.
2
+ > Brought to you by [Bytebase](https://www.bytebase.com/), open-source database governance platform.
3
3
 
4
4
  <p align="center">
5
5
  <a href="https://dbhub.ai/" target="_blank">
@@ -30,7 +30,7 @@
30
30
  MCP Clients MCP Server Databases
31
31
  ```
32
32
 
33
- DBHub is a zero-dependency, token efficient MCP server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
33
+ DBHub is a token efficient, zero-dependency MCP server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
34
34
 
35
35
  - **Local Development First**: Zero dependency, token efficient with a minimal set of MCP tools to maximize context window
36
36
  - **Multi-Database**: PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite through a single interface
@@ -49,7 +49,7 @@ DBHub implements MCP tools for database operations:
49
49
  - **[execute_sql](https://dbhub.ai/tools/execute-sql)**: Execute SQL queries with transaction support and safety controls
50
50
  - **[search_objects](https://dbhub.ai/tools/search-objects)**: Search and explore database schemas, tables, columns, indexes, and procedures with progressive disclosure
51
51
  - **[explain_sql](https://dbhub.ai/tools/explain-sql)** (opt-in): Show a query's execution plan without running it
52
- - **[health_check](https://dbhub.ai/tools/health-check)** (opt-in, PostgreSQL/MySQL/MariaDB/SQL Server for now): Report connection pool state and buffer cache hit ratio
52
+ - **[health_check](https://dbhub.ai/tools/health-check)** (opt-in): Report connection pool state and buffer cache hit ratio
53
53
  - **[Custom Tools](https://dbhub.ai/tools/custom-tools)**: Define reusable, parameterized SQL operations in your `dbhub.toml` configuration file
54
54
 
55
55
  ## Workbench
@@ -8,7 +8,7 @@ import {
8
8
  getDefaultPortForType,
9
9
  parseConnectionInfoFromDSN,
10
10
  stripCommentsAndStrings
11
- } from "./chunk-JEZZN2YZ.js";
11
+ } from "./chunk-WIQ4DWGY.js";
12
12
 
13
13
  // src/tools/builtin-tools.ts
14
14
  var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
@@ -805,6 +805,21 @@ function resolveAllowedHosts() {
805
805
  function splitHostList(value) {
806
806
  return value.split(",").map((h) => h.trim()).filter((h) => h.length > 0);
807
807
  }
808
+ function resolveAuthTokens() {
809
+ const args = parseCommandLineArgs();
810
+ const cliValue = requireFlagValue("auth-token", args, "secret1,secret2");
811
+ if (cliValue !== void 0) {
812
+ return { tokens: splitTokenList(cliValue), source: "command line argument" };
813
+ }
814
+ const envValue = process.env.DBHUB_AUTH_TOKEN?.trim();
815
+ if (envValue) {
816
+ return { tokens: splitTokenList(envValue), source: "environment variable" };
817
+ }
818
+ return { tokens: [], source: "default" };
819
+ }
820
+ function splitTokenList(value) {
821
+ return value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
822
+ }
808
823
  function redactDSN(dsn) {
809
824
  try {
810
825
  const url = new URL(dsn);
@@ -2492,6 +2507,7 @@ export {
2492
2507
  resolvePort,
2493
2508
  resolveHost,
2494
2509
  resolveAllowedHosts,
2510
+ resolveAuthTokens,
2495
2511
  resolveSourceConfigs,
2496
2512
  BUILTIN_TOOL_EXECUTE_SQL,
2497
2513
  BUILTIN_TOOL_SEARCH_OBJECTS,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  stripCommentsAndStrings
3
- } from "./chunk-JEZZN2YZ.js";
3
+ } from "./chunk-WIQ4DWGY.js";
4
4
 
5
5
  // src/utils/allowed-keywords.ts
6
6
  var allowedKeywords = {
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-FU2ZJE4E.js";
5
5
  import {
6
6
  obfuscateDSNPassword
7
- } from "./chunk-JEZZN2YZ.js";
7
+ } from "./chunk-WIQ4DWGY.js";
8
8
 
9
9
  // src/connectors/mysql-family-health-check.ts
10
10
  async function getMySQLFamilyHealthCheck(query) {
@@ -111,49 +111,36 @@ function isMultiStatementResult(results) {
111
111
  const firstElement = results[0];
112
112
  return isMetadataObject(firstElement) || Array.isArray(firstElement);
113
113
  }
114
- function extractRowsFromMultiStatement(results) {
115
- if (!Array.isArray(results)) {
116
- return [];
117
- }
118
- const allRows = [];
119
- for (const result of results) {
114
+ function buildResultSetsFromMultiStatement(results, statementTexts) {
115
+ const sql = statementTexts?.length === results.length ? statementTexts : void 0;
116
+ return results.map((result, index) => {
120
117
  if (Array.isArray(result)) {
121
- allRows.push(...result);
118
+ return { sql: sql?.[index], rows: result, rowCount: result.length };
122
119
  }
123
- }
124
- return allRows;
120
+ if (isMetadataObject(result)) {
121
+ return { sql: sql?.[index], rows: [], rowCount: result.affectedRows || 0 };
122
+ }
123
+ return { sql: sql?.[index], rows: [], rowCount: 0 };
124
+ });
125
125
  }
126
- function extractAffectedRows(results) {
126
+ function parseQueryResultSets(results, statementTexts) {
127
+ const singleStatementSql = statementTexts?.length === 1 ? statementTexts[0] : void 0;
127
128
  if (isMetadataObject(results)) {
128
- return results.affectedRows || 0;
129
+ return [{ sql: singleStatementSql, rows: [], rowCount: results.affectedRows || 0 }];
129
130
  }
130
131
  if (!Array.isArray(results)) {
131
- return 0;
132
+ return [{ sql: singleStatementSql, rows: [], rowCount: 0 }];
132
133
  }
133
134
  if (isMultiStatementResult(results)) {
134
- let totalAffected = 0;
135
- for (const result of results) {
136
- if (isMetadataObject(result)) {
137
- totalAffected += result.affectedRows || 0;
138
- } else if (Array.isArray(result)) {
139
- totalAffected += result.length;
140
- }
141
- }
142
- return totalAffected;
135
+ return buildResultSetsFromMultiStatement(results, statementTexts);
143
136
  }
144
- return results.length;
145
- }
146
- function parseQueryResults(results) {
147
- if (!Array.isArray(results)) {
148
- return [];
149
- }
150
- if (isMultiStatementResult(results)) {
151
- return extractRowsFromMultiStatement(results);
152
- }
153
- return results;
137
+ return [{ sql: singleStatementSql, rows: results, rowCount: results.length }];
154
138
  }
155
139
 
156
140
  // src/utils/readonly-transaction.ts
141
+ function isClientSideTimeout(error) {
142
+ return typeof error === "object" && error !== null && error.code === "PROTOCOL_SEQUENCE_TIMEOUT";
143
+ }
157
144
  async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
158
145
  if (!readonly) {
159
146
  return execute();
@@ -166,9 +153,11 @@ async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransacti
166
153
  await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
167
154
  return result;
168
155
  } catch (error) {
169
- try {
170
- await conn.query("ROLLBACK");
171
- } catch {
156
+ if (!isClientSideTimeout(error)) {
157
+ try {
158
+ await conn.query("ROLLBACK");
159
+ } catch {
160
+ }
172
161
  }
173
162
  throw error;
174
163
  }
@@ -183,8 +172,8 @@ export {
183
172
  getMySQLFamilyHealthCheck,
184
173
  MissingDatabaseError,
185
174
  requireDatabaseInDSN,
186
- extractAffectedRows,
187
- parseQueryResults,
175
+ parseQueryResultSets,
176
+ isClientSideTimeout,
188
177
  withReadOnlyTransaction,
189
178
  isTiDBVersion
190
179
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
+ blankCommentsAndStrings,
2
3
  stripCommentsAndStrings
3
- } from "./chunk-JEZZN2YZ.js";
4
+ } from "./chunk-WIQ4DWGY.js";
4
5
 
5
6
  // src/utils/sql-row-limiter.ts
6
7
  var SQLRowLimiter = class {
@@ -70,17 +71,77 @@ var SQLRowLimiter = class {
70
71
  LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
71
72
  }
72
73
  }
74
+ /**
75
+ * Scan blanked (comment/string-free, length-preserving) SQL for parenthesis
76
+ * depth, invoking onMatch for every regex hit at depth 0 (i.e. not nested
77
+ * inside a subquery, function call, or window OVER clause).
78
+ */
79
+ static scanTopLevel(blankedSQL, regex, onMatch) {
80
+ let depth = 0;
81
+ let m;
82
+ while ((m = regex.exec(blankedSQL)) !== null) {
83
+ const token = m[0];
84
+ if (token === "(") {
85
+ depth++;
86
+ } else if (token === ")") {
87
+ depth--;
88
+ } else if (depth === 0) {
89
+ onMatch(m);
90
+ }
91
+ }
92
+ }
93
+ /**
94
+ * Check if a SQL statement combines multiple SELECTs with a set operator
95
+ * (UNION [ALL], INTERSECT, EXCEPT) at the top level — i.e. not nested
96
+ * inside a subquery already wrapped in parentheses. Strips comments and
97
+ * string literals first to avoid false positives.
98
+ */
99
+ static hasSetOperator(sql) {
100
+ const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
101
+ let found = false;
102
+ this.scanTopLevel(blankedSQL, /\(|\)|\bunion\b|\bintersect\b|\bexcept\b/gi, () => {
103
+ found = true;
104
+ });
105
+ return found;
106
+ }
107
+ /**
108
+ * Find the start index of a top-level trailing ORDER BY clause (not one
109
+ * nested inside a subquery or a window function's OVER (...) clause).
110
+ * Returns -1 if none exists. Operates on the original (non-blanked) SQL
111
+ * length, since blankCommentsAndStrings preserves length/position.
112
+ */
113
+ static findTopLevelOrderByIndex(sql) {
114
+ const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
115
+ let lastIndex = -1;
116
+ this.scanTopLevel(blankedSQL, /\(|\)|\border\s+by\b/gi, (m) => {
117
+ lastIndex = m.index;
118
+ });
119
+ return lastIndex;
120
+ }
73
121
  /**
74
122
  * Add or modify TOP clause in a SQL statement (SQL Server)
75
123
  */
76
124
  static applyTopToQuery(sql, maxRows) {
125
+ if (this.hasSetOperator(sql)) {
126
+ const trimmed = sql.trim();
127
+ const hasSemicolon = trimmed.endsWith(";");
128
+ const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
129
+ const orderByIndex = this.findTopLevelOrderByIndex(sqlWithoutSemicolon);
130
+ if (orderByIndex !== -1) {
131
+ const innerSql = sqlWithoutSemicolon.slice(0, orderByIndex).trimEnd();
132
+ const orderByClause = sqlWithoutSemicolon.slice(orderByIndex).trim();
133
+ return `SELECT TOP ${maxRows} * FROM (${innerSql}
134
+ ) AS subq ${orderByClause}${hasSemicolon ? ";" : ""}`;
135
+ }
136
+ return `SELECT TOP ${maxRows} * FROM (${sqlWithoutSemicolon}
137
+ ) AS subq${hasSemicolon ? ";" : ""}`;
138
+ }
77
139
  const existingTop = this.extractTopValue(sql);
78
140
  if (existingTop !== null) {
79
141
  const effectiveTop = Math.min(existingTop, maxRows);
80
142
  return sql.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
81
- } else {
82
- return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
83
143
  }
144
+ return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
84
145
  }
85
146
  /**
86
147
  * Check if a LIMIT clause uses a parameter placeholder (not a literal number).
@@ -462,6 +462,21 @@ function stripCommentsAndStrings(sql, dialect) {
462
462
  }
463
463
  return parts.join("");
464
464
  }
465
+ function blankCommentsAndStrings(sql, dialect) {
466
+ const scanToken = getScanner(dialect);
467
+ let result = "";
468
+ let i = 0;
469
+ while (i < sql.length) {
470
+ const token = scanToken(sql, i);
471
+ if (token.type === TokenType.Plain) {
472
+ result += sql[i];
473
+ } else {
474
+ result += " ".repeat(token.end - i);
475
+ }
476
+ i = token.end;
477
+ }
478
+ return result;
479
+ }
465
480
  function splitSQLStatements(sql, dialect) {
466
481
  const scanToken = getScanner(dialect);
467
482
  const statements = [];
@@ -495,5 +510,6 @@ export {
495
510
  getDatabaseTypeFromDSN,
496
511
  getDefaultPortForType,
497
512
  stripCommentsAndStrings,
513
+ blankCommentsAndStrings,
498
514
  splitSQLStatements
499
515
  };
package/dist/index.js CHANGED
@@ -12,19 +12,20 @@ import {
12
12
  loadTomlConfig,
13
13
  mapArgumentsToArray,
14
14
  resolveAllowedHosts,
15
+ resolveAuthTokens,
15
16
  resolveHost,
16
17
  resolvePort,
17
18
  resolveSourceConfigs,
18
19
  resolveTomlConfigPath,
19
20
  resolveTransport
20
- } from "./chunk-GSSLHJB6.js";
21
+ } from "./chunk-CUBIR72I.js";
21
22
  import {
22
23
  allowedKeywords,
23
24
  getFirstKeyword,
24
25
  hasEscapeHatchFunction,
25
26
  isReadOnlySQL,
26
27
  sqlServerDynamicSqlPattern
27
- } from "./chunk-EPOKWSFC.js";
28
+ } from "./chunk-PBN2UXB5.js";
28
29
  import {
29
30
  loadConnectors
30
31
  } from "./chunk-WVVMH6FJ.js";
@@ -38,7 +39,7 @@ import {
38
39
  parseConnectionInfoFromDSN,
39
40
  splitSQLStatements,
40
41
  stripCommentsAndStrings
41
- } from "./chunk-JEZZN2YZ.js";
42
+ } from "./chunk-WIQ4DWGY.js";
42
43
 
43
44
  // src/server.ts
44
45
  import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
@@ -219,6 +220,13 @@ function normalizeSourceId(id) {
219
220
  function getEffectiveSourceId(sourceId) {
220
221
  return sourceId || "default";
221
222
  }
223
+ function toStatementsPayload(resultSets) {
224
+ return resultSets.map((set) => ({
225
+ sql: set.sql,
226
+ rows: set.rows,
227
+ count: set.rowCount
228
+ }));
229
+ }
222
230
  function resolveTrackedToolName(sourceId, baseName) {
223
231
  const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
224
232
  if (isSingleSource) {
@@ -288,8 +296,7 @@ function createExecuteSqlToolHandler(sourceId) {
288
296
  };
289
297
  result = await connector.executeSQL(sql, executeOptions);
290
298
  const responseData = {
291
- rows: result.rows,
292
- count: result.rowCount,
299
+ statements: toStatementsPayload(result.resultSets),
293
300
  source_id: effectiveSourceId,
294
301
  ...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
295
302
  };
@@ -348,8 +355,9 @@ async function getTableRowCount(connector, tableName, schemaName) {
348
355
  const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
349
356
  const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
350
357
  const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
351
- if (result.rows && result.rows.length > 0) {
352
- 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);
353
361
  }
354
362
  } catch (error) {
355
363
  return null;
@@ -852,8 +860,8 @@ function createExplainSqlToolHandler(sourceId) {
852
860
  const explainStatement = buildExplainStatement(connector.id, sql);
853
861
  result = await connector.executeSQL(explainStatement, { readonly: true });
854
862
  const responseData = {
855
- rows: result.rows,
856
- count: result.rowCount,
863
+ rows: result.resultSets[0].rows,
864
+ count: result.resultSets[0].rowCount,
857
865
  source_id: effectiveSourceId,
858
866
  ...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
859
867
  };
@@ -1036,7 +1044,7 @@ function getExplainSqlMetadata(sourceId) {
1036
1044
  "explain_sql",
1037
1045
  "Explain Query Plan"
1038
1046
  );
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.`;
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).`;
1040
1048
  const annotations = {
1041
1049
  title,
1042
1050
  readOnlyHint: true,
@@ -1057,7 +1065,7 @@ function getHealthCheckMetadata(sourceId) {
1057
1065
  "health_check",
1058
1066
  "Health Check"
1059
1067
  );
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.`;
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).`;
1061
1069
  const annotations = {
1062
1070
  title,
1063
1071
  readOnlyHint: true,
@@ -1107,13 +1115,13 @@ function buildSearchObjectsTool(sourceId) {
1107
1115
  name: "object_type",
1108
1116
  type: "string",
1109
1117
  required: true,
1110
- description: "Object type to search: schema, table, view, column, procedure, function, index"
1118
+ description: "Object type to search"
1111
1119
  },
1112
1120
  {
1113
1121
  name: "pattern",
1114
1122
  type: "string",
1115
1123
  required: false,
1116
- description: "LIKE pattern (% = any chars, _ = one char). Default: %"
1124
+ description: "LIKE pattern (% = any chars, _ = one char)"
1117
1125
  },
1118
1126
  {
1119
1127
  name: "schema",
@@ -1137,7 +1145,7 @@ function buildSearchObjectsTool(sourceId) {
1137
1145
  name: "limit",
1138
1146
  type: "integer",
1139
1147
  required: false,
1140
- description: "Max results (default: 100, max: 1000)"
1148
+ description: "Max results"
1141
1149
  }
1142
1150
  ],
1143
1151
  readonly: true
@@ -1267,8 +1275,7 @@ function createCustomToolHandler(toolConfig) {
1267
1275
  paramValues
1268
1276
  );
1269
1277
  const responseData = {
1270
- rows: result.rows,
1271
- count: result.rowCount,
1278
+ statements: toStatementsPayload(result.resultSets),
1272
1279
  source_id: toolConfig.source
1273
1280
  };
1274
1281
  return createToolSuccessResponse(responseData);
@@ -1822,6 +1829,32 @@ function validateOrigin(originHeader, hostHeader, allowedHosts) {
1822
1829
  return { ok: true };
1823
1830
  }
1824
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
+
1825
1858
  // src/server.ts
1826
1859
  var __filename = fileURLToPath(import.meta.url);
1827
1860
  var __dirname = path.dirname(__filename);
@@ -1876,7 +1909,7 @@ See documentation for more details on configuring database connections.
1876
1909
  const sources = sourceConfigsData.sources;
1877
1910
  console.error(`Configuration source: ${sourceConfigsData.source}`);
1878
1911
  await connectorManager.connectWithSources(sources);
1879
- const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-JYRRNC3E.js");
1912
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-XMEJF7WW.js");
1880
1913
  initializeToolRegistry2({
1881
1914
  sources: sourceConfigsData.sources,
1882
1915
  tools: sourceConfigsData.tools
@@ -1906,6 +1939,7 @@ See documentation for more details on configuring database connections.
1906
1939
  const port = transportData.type === "http" ? resolvePort().port : null;
1907
1940
  const host = transportData.type === "http" ? resolveHost().host : null;
1908
1941
  const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
1942
+ const { tokens: authTokens, source: authTokenSource } = resolveAuthTokens();
1909
1943
  const activeModes = [];
1910
1944
  const modeDescriptions = [];
1911
1945
  const isDemo = isDemoMode();
@@ -1940,18 +1974,26 @@ See documentation for more details on configuring database connections.
1940
1974
  }
1941
1975
  res.header("Access-Control-Allow-Origin", origin || "http://localhost");
1942
1976
  res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1943
- 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");
1944
1978
  res.header("Access-Control-Allow-Credentials", "true");
1945
1979
  if (req.method === "OPTIONS") {
1946
1980
  return res.sendStatus(200);
1947
1981
  }
1948
1982
  next();
1949
1983
  });
1950
- const frontendPath = path.join(__dirname, "public");
1951
- app.use(express.static(frontendPath));
1952
1984
  app.get("/healthz", (req, res) => {
1953
1985
  res.status(200).send("OK");
1954
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));
1955
1997
  app.get("/api/sources", listSources);
1956
1998
  app.get("/api/sources/:sourceId", getSource);
1957
1999
  app.get("/api/requests", listRequests);
@@ -1983,6 +2025,11 @@ See documentation for more details on configuring database connections.
1983
2025
  } else {
1984
2026
  console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
1985
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
+ }
1986
2033
  if (process.env.NODE_ENV === "development") {
1987
2034
  console.error("Development mode detected!");
1988
2035
  console.error(" Workbench dev server (with HMR): http://localhost:5173");
@@ -2017,11 +2064,11 @@ See documentation for more details on configuring database connections.
2017
2064
 
2018
2065
  // src/index.ts
2019
2066
  var connectorModules = [
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" }
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" }
2025
2072
  ];
2026
2073
  loadConnectors(connectorModules).then(() => main()).catch((error) => {
2027
2074
  console.error("Fatal error:", error);
@@ -1,12 +1,11 @@
1
1
  import {
2
2
  MissingDatabaseError,
3
- extractAffectedRows,
4
3
  getMySQLFamilyHealthCheck,
5
4
  isTiDBVersion,
6
- parseQueryResults,
5
+ parseQueryResultSets,
7
6
  requireDatabaseInDSN,
8
7
  withReadOnlyTransaction
9
- } from "./chunk-NIILHXTG.js";
8
+ } from "./chunk-QEFMUQJ2.js";
10
9
  import "./chunk-FU2ZJE4E.js";
11
10
  import {
12
11
  quoteIdentifier
@@ -14,13 +13,13 @@ import {
14
13
  import {
15
14
  SQLRowLimiter,
16
15
  closeQuietly
17
- } from "./chunk-S2V6RZ3C.js";
16
+ } from "./chunk-RSB7DPO2.js";
18
17
  import {
19
18
  ConnectorRegistry,
20
19
  SafeURL,
21
20
  obfuscateDSNPassword,
22
21
  splitSQLStatements
23
- } from "./chunk-JEZZN2YZ.js";
22
+ } from "./chunk-WIQ4DWGY.js";
24
23
 
25
24
  // src/connectors/mariadb/index.ts
26
25
  import * as mariadb from "mariadb";
@@ -505,9 +504,9 @@ var MariaDBConnector = class _MariaDBConnector {
505
504
  options.readonly,
506
505
  this.supportsReadOnlyTransaction,
507
506
  async () => {
507
+ const statements = splitSQLStatements(sql, "mariadb");
508
508
  let processedSQL = sql;
509
509
  if (options.maxRows) {
510
- const statements = splitSQLStatements(sql, "mariadb");
511
510
  const processedStatements = statements.map(
512
511
  (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
513
512
  );
@@ -522,9 +521,8 @@ var MariaDBConnector = class _MariaDBConnector {
522
521
  } else {
523
522
  results = await conn.query(processedSQL);
524
523
  }
525
- const rows = parseQueryResults(results);
526
- const rowCount = extractAffectedRows(results);
527
- return { rows, rowCount };
524
+ const resultSets = parseQueryResultSets(results, statements);
525
+ return { resultSets };
528
526
  }
529
527
  );
530
528
  } finally {