@zz1996/dbhub-dameng 0.1.7 → 1.2.5-dameng.621

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.
Files changed (32) hide show
  1. package/README.md +126 -137
  2. package/dbhub.dameng.toml.example +4 -11
  3. package/dist/{chunk-SQA2ISDE.js → chunk-2JL4KCP5.js} +21 -119
  4. package/dist/chunk-3RJRH3CN.js +121 -0
  5. package/dist/chunk-4276LPPU.js +179 -0
  6. package/dist/chunk-FU2ZJE4E.js +16 -0
  7. package/dist/chunk-HDPZAIDL.js +155 -0
  8. package/dist/{chunk-2A2QF3CS.js → chunk-HGU7ILEZ.js} +215 -68
  9. package/dist/{chunk-IPK7BYBL.js → chunk-JFWX35TB.js} +0 -1
  10. package/dist/chunk-O3PEWKBB.js +302 -0
  11. package/dist/dameng-NKF7NZKE.js +250 -0
  12. package/dist/index.js +461 -251
  13. package/dist/{mariadb-7F72IRB4.js → mariadb-MMC6WMEG.js} +86 -53
  14. package/dist/{mysql-A43SL7UM.js → mysql-TESH7KKV.js} +126 -50
  15. package/dist/{postgres-VFNLBWMF.js → postgres-C24SBQ4I.js} +154 -36
  16. package/dist/public/assets/index-BJ-1UrcV.css +1 -0
  17. package/dist/public/assets/index-BdBaFSHd.js +148 -0
  18. package/dist/public/assets/postgres-BpcazhJg.svg +22 -0
  19. package/dist/public/assets/sqlserver-ByfFYYpV.svg +11 -0
  20. package/dist/public/favicon.svg +57 -0
  21. package/dist/public/index.html +14 -0
  22. package/dist/public/logo-full-light.svg +58 -0
  23. package/dist/{registry-XSX2VZTD.js → registry-WVSFVS35.js} +3 -2
  24. package/dist/{sqlite-IOUAYHGE.js → sqlite-7BNGDMBZ.js} +57 -42
  25. package/dist/sqlserver-KZPPK67Q.js +1042 -0
  26. package/package.json +22 -65
  27. package/dist/chunk-RTB262PR.js +0 -60
  28. package/dist/chunk-ZNQTMARG.js +0 -127
  29. package/dist/dameng-5KYI5JBK.js +0 -898
  30. package/dist/sqlserver-OE4SFH4B.js +0 -587
  31. package/docs/README.md +0 -14
  32. package/docs/dameng-connector-plan.md +0 -143
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,31 +12,41 @@ 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-2A2QF3CS.js";
21
+ } from "./chunk-HGU7ILEZ.js";
19
22
  import {
20
23
  loadConnectors
21
24
  } from "./chunk-WVVMH6FJ.js";
22
25
  import {
23
26
  quoteQualifiedIdentifier
24
- } from "./chunk-IPK7BYBL.js";
27
+ } from "./chunk-JFWX35TB.js";
25
28
  import {
26
- ConnectorRegistry,
27
29
  getDatabaseTypeFromDSN,
28
30
  getDefaultPortForType,
29
- parseConnectionInfoFromDSN,
31
+ parseConnectionInfoFromDSN
32
+ } from "./chunk-3RJRH3CN.js";
33
+ import {
34
+ allowedKeywords,
35
+ getFirstKeyword,
36
+ hasEscapeHatchFunction,
37
+ isReadOnlySQL,
38
+ sqlServerDynamicSqlPattern
39
+ } from "./chunk-HDPZAIDL.js";
40
+ import {
41
+ ConnectorRegistry,
30
42
  splitSQLStatements,
31
43
  stripCommentsAndStrings
32
- } from "./chunk-SQA2ISDE.js";
44
+ } from "./chunk-2JL4KCP5.js";
33
45
 
34
46
  // src/server.ts
35
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
36
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
37
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
47
+ import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
48
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
49
+ import { toNodeHandler } from "@modelcontextprotocol/node";
38
50
  import express from "express";
39
51
  import http from "http";
40
52
  import path from "path";
@@ -95,98 +107,51 @@ function createToolSuccessResponse(data, meta = {}) {
95
107
  };
96
108
  }
97
109
 
98
- // src/utils/allowed-keywords.ts
99
- var allowedKeywords = {
100
- postgres: ["select", "with", "explain", "show"],
101
- mysql: ["select", "with", "explain", "show", "describe", "desc"],
102
- mariadb: ["select", "with", "explain", "show", "describe", "desc"],
103
- sqlite: ["select", "with", "explain", "pragma"],
104
- // SQL Server has no native EXPLAIN statement; the connector translates a
105
- // leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
106
- sqlserver: ["select", "with", "explain"],
107
- dameng: ["select", "with", "explain", "describe", "desc"]
110
+ // src/utils/sql-access-policy.ts
111
+ var CLASS_SEVERITY = {
112
+ read: 0,
113
+ dml: 1,
114
+ ddl: 2,
115
+ admin: 3,
116
+ unknown: 4
108
117
  };
109
- var mutatingKeywords = [
110
- "insert",
111
- "update",
112
- "delete",
113
- "drop",
114
- "alter",
115
- "create",
116
- "truncate",
117
- "merge",
118
- "grant",
119
- "revoke",
120
- "rename"
121
- ];
122
- var mutatingPattern = new RegExp(
123
- `\\b(?:${mutatingKeywords.join("|")})\\b`,
124
- "i"
125
- );
126
- var mutatingPatternWithReplace = new RegExp(
127
- `\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
128
- "i"
129
- );
130
- var mutatingPatterns = {
131
- postgres: mutatingPattern,
132
- mysql: mutatingPatternWithReplace,
133
- mariadb: mutatingPatternWithReplace,
134
- sqlite: mutatingPatternWithReplace,
135
- sqlserver: mutatingPattern,
136
- dameng: mutatingPattern
137
- };
138
- var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
139
- var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
140
- "table_info",
141
- "index_info",
142
- "index_list",
143
- "foreign_key_list"
144
- ]);
145
- var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
146
- var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
147
- function isReadOnlySQL(sql, connectorType) {
148
- return checkReadOnly(
149
- stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(),
150
- connectorType
151
- );
118
+ var ddlPattern = /\b(?:create|alter|drop|truncate|rename)\b/i;
119
+ var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|delayed)\s+)?into)\b/i;
120
+ var grantRevokePattern = /\b(?:grant|revoke)\b/i;
121
+ function isAdminStatement(stripped, connectorType) {
122
+ if (grantRevokePattern.test(stripped)) return true;
123
+ if (hasEscapeHatchFunction(stripped, connectorType)) return true;
124
+ return connectorType === "sqlserver" && sqlServerDynamicSqlPattern.test(stripped);
152
125
  }
153
- function checkReadOnly(cleanedSQL, connectorType) {
154
- if (!cleanedSQL) {
155
- return false;
156
- }
157
- const firstWord = cleanedSQL.match(/\S+/)?.[0] ?? "";
158
- const keywordList = allowedKeywords[connectorType] || [];
159
- if (!keywordList.includes(firstWord)) {
160
- return false;
161
- }
162
- if (firstWord === "with") {
163
- const pattern = mutatingPatterns[connectorType] ?? mutatingPattern;
164
- if (pattern.test(cleanedSQL)) {
165
- return false;
166
- }
167
- }
168
- if (firstWord === "pragma" && connectorType === "sqlite") {
169
- if (cleanedSQL.includes("=")) {
170
- return false;
171
- }
172
- const parenMatch = cleanedSQL.match(sqlitePragmaParenPattern);
173
- if (parenMatch && !sqliteReadOnlyArgPragmas.has(parenMatch[1])) {
174
- return false;
175
- }
176
- }
177
- if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
178
- return false;
179
- }
180
- if (firstWord === "explain") {
181
- const m = explainAnalyzePattern.exec(cleanedSQL);
182
- if (m) {
183
- const afterExplain = cleanedSQL.slice(m[0].length).trim();
184
- if (afterExplain && !checkReadOnly(afterExplain, connectorType)) {
185
- return false;
186
- }
126
+ function policyFromReadonly(readonly) {
127
+ const write = readonly === true ? "deny" : "allow";
128
+ return { read: "allow", dml: write, ddl: write, admin: write, unknown: write };
129
+ }
130
+ function isReadOnlyPolicy(policy) {
131
+ return policy.dml === "deny" && policy.ddl === "deny" && policy.admin === "deny" && policy.unknown === "deny";
132
+ }
133
+ function classifyStatement(sql, connectorType) {
134
+ if (isReadOnlySQL(sql, connectorType)) {
135
+ return "read";
136
+ }
137
+ const stripped = stripCommentsAndStrings(sql, connectorType);
138
+ if (isAdminStatement(stripped, connectorType)) return "admin";
139
+ if (ddlPattern.test(stripped)) return "ddl";
140
+ if (dmlPattern.test(stripped)) return "dml";
141
+ return "unknown";
142
+ }
143
+ function classifySQL(sql, connectorType) {
144
+ let strictest = "read";
145
+ for (const statement of splitSQLStatements(sql, connectorType)) {
146
+ const cls = classifyStatement(statement, connectorType);
147
+ if (CLASS_SEVERITY[cls] > CLASS_SEVERITY[strictest]) {
148
+ strictest = cls;
187
149
  }
188
150
  }
189
- return true;
151
+ return strictest;
152
+ }
153
+ function sqlVerdict(policy, sql, connectorType) {
154
+ return policy[classifySQL(sql, connectorType)];
190
155
  }
191
156
 
192
157
  // src/requests/store.ts
@@ -240,18 +205,41 @@ var RequestStore = class {
240
205
  var requestStore = new RequestStore();
241
206
 
242
207
  // src/utils/client-identifier.ts
243
- function getClientIdentifier(extra) {
244
- const userAgent = extra?.requestInfo?.headers?.["user-agent"];
208
+ function getClientIdentifier(ctx) {
209
+ const userAgent = ctx?.http?.req?.headers.get("user-agent");
245
210
  if (userAgent) {
246
211
  return userAgent;
247
212
  }
248
213
  return "stdio";
249
214
  }
250
215
 
216
+ // src/utils/normalize-id.ts
217
+ function normalizeSourceId(id) {
218
+ return id.replace(/[^a-zA-Z0-9]/g, "_");
219
+ }
220
+
251
221
  // src/utils/tool-handler-helpers.ts
252
222
  function getEffectiveSourceId(sourceId) {
253
223
  return sourceId || "default";
254
224
  }
225
+ function toStatementsPayload(resultSets) {
226
+ return resultSets.map((set) => ({
227
+ sql: set.sql,
228
+ rows: set.rows,
229
+ count: set.rowCount,
230
+ // Only present when a max_rows cap actually cut off rows, so consumers
231
+ // can tell a capped result from a table with exactly max_rows rows
232
+ // (and complete results don't pay the extra tokens).
233
+ ...set.truncated ? { truncated: true } : {}
234
+ }));
235
+ }
236
+ function resolveTrackedToolName(sourceId, baseName) {
237
+ const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
238
+ if (isSingleSource) {
239
+ return baseName;
240
+ }
241
+ return `${baseName}_${normalizeSourceId(getEffectiveSourceId(sourceId))}`;
242
+ }
255
243
  function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
256
244
  return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
257
245
  }
@@ -287,10 +275,7 @@ function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
287
275
  var executeSqlSchema = {
288
276
  sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
289
277
  };
290
- function areAllStatementsReadOnly(sql, connectorType) {
291
- const statements = splitSQLStatements(sql, connectorType);
292
- return statements.every((statement) => isReadOnlySQL(statement, connectorType));
293
- }
278
+ var executeSqlInputSchema = z.object(executeSqlSchema);
294
279
  function createExecuteSqlToolHandler(sourceId) {
295
280
  return async (args, extra) => {
296
281
  const { sql } = args;
@@ -305,20 +290,19 @@ function createExecuteSqlToolHandler(sourceId) {
305
290
  const actualSourceId = connector.getId();
306
291
  const registry = getToolRegistry();
307
292
  const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
308
- const isReadonly = toolConfig?.readonly === true;
309
- if (isReadonly && !areAllStatementsReadOnly(sql, connector.id)) {
293
+ const policy = policyFromReadonly(toolConfig?.readonly);
294
+ if (sqlVerdict(policy, sql, connector.id) === "deny") {
310
295
  errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
311
296
  success = false;
312
297
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
313
298
  }
314
299
  const executeOptions = {
315
- readonly: toolConfig?.readonly,
300
+ readonly: isReadOnlyPolicy(policy),
316
301
  maxRows: toolConfig?.max_rows
317
302
  };
318
303
  result = await connector.executeSQL(sql, executeOptions);
319
304
  const responseData = {
320
- rows: result.rows,
321
- count: result.rowCount,
305
+ statements: toStatementsPayload(result.resultSets),
322
306
  source_id: effectiveSourceId,
323
307
  ...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
324
308
  };
@@ -355,6 +339,7 @@ var searchDatabaseObjectsSchema = {
355
339
  detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
356
340
  limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
357
341
  };
342
+ var searchDatabaseObjectsInputSchema = z2.object(searchDatabaseObjectsSchema);
358
343
  function likePatternToRegex(pattern) {
359
344
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
360
345
  return new RegExp(`^${escaped}$`, "i");
@@ -375,9 +360,10 @@ async function getTableRowCount(connector, tableName, schemaName) {
375
360
  }
376
361
  const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
377
362
  const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
378
- const result = await connector.executeSQL(countQuery, { maxRows: 1 });
379
- if (result.rows && result.rows.length > 0) {
380
- return Number(result.rows[0].count || result.rows[0].COUNT || 0);
363
+ const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
364
+ const rows = result.resultSets[0]?.rows;
365
+ if (rows && rows.length > 0) {
366
+ return Number(rows[0].count || rows[0].COUNT || 0);
381
367
  }
382
368
  } catch (error) {
383
369
  return null;
@@ -431,25 +417,23 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
431
417
  for (const schemaName of schemasToSearch) {
432
418
  if (results.length >= limit) break;
433
419
  try {
434
- const searchableConnector = connector;
435
- const matched = searchableConnector.searchTables ? await searchableConnector.searchTables(pattern, schemaName, limit - results.length) : (await connector.getTables(schemaName)).filter((table) => regex.test(table)).map((name) => ({ name, schema: schemaName }));
436
- for (const tableMatch of matched) {
420
+ const tables = await connector.getTables(schemaName);
421
+ const matched = tables.filter((table) => regex.test(table));
422
+ for (const tableName of matched) {
437
423
  if (results.length >= limit) break;
438
- const tableName = tableMatch.name;
439
- const matchedSchema = tableMatch.schema;
440
424
  if (detailLevel === "names") {
441
425
  results.push({
442
426
  name: tableName,
443
- schema: matchedSchema
427
+ schema: schemaName
444
428
  });
445
429
  } else if (detailLevel === "summary") {
446
430
  try {
447
- const columns = await connector.getTableSchema(tableName, matchedSchema);
448
- const rowCount = await getTableRowCount(connector, tableName, matchedSchema);
449
- const comment = await getTableComment(connector, tableName, matchedSchema);
431
+ const columns = await connector.getTableSchema(tableName, schemaName);
432
+ const rowCount = await getTableRowCount(connector, tableName, schemaName);
433
+ const comment = await getTableComment(connector, tableName, schemaName);
450
434
  results.push({
451
435
  name: tableName,
452
- schema: matchedSchema,
436
+ schema: schemaName,
453
437
  column_count: columns.length,
454
438
  row_count: rowCount,
455
439
  ...comment ? { comment } : {}
@@ -457,20 +441,20 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
457
441
  } catch (error) {
458
442
  results.push({
459
443
  name: tableName,
460
- schema: matchedSchema,
444
+ schema: schemaName,
461
445
  column_count: null,
462
446
  row_count: null
463
447
  });
464
448
  }
465
449
  } else {
466
450
  try {
467
- const columns = await connector.getTableSchema(tableName, matchedSchema);
468
- const indexes = await connector.getTableIndexes(tableName, matchedSchema);
469
- const rowCount = await getTableRowCount(connector, tableName, matchedSchema);
470
- const comment = await getTableComment(connector, tableName, matchedSchema);
451
+ const columns = await connector.getTableSchema(tableName, schemaName);
452
+ const indexes = await connector.getTableIndexes(tableName, schemaName);
453
+ const rowCount = await getTableRowCount(connector, tableName, schemaName);
454
+ const comment = await getTableComment(connector, tableName, schemaName);
471
455
  results.push({
472
456
  name: tableName,
473
- schema: matchedSchema,
457
+ schema: schemaName,
474
458
  column_count: columns.length,
475
459
  row_count: rowCount,
476
460
  ...comment ? { comment } : {},
@@ -491,7 +475,7 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
491
475
  } catch (error) {
492
476
  results.push({
493
477
  name: tableName,
494
- schema: matchedSchema,
478
+ schema: schemaName,
495
479
  error: `Unable to fetch full details: ${error.message}`
496
480
  });
497
481
  }
@@ -510,7 +494,7 @@ async function searchViews(connector, pattern, schemaFilter, detailLevel, limit)
510
494
  if (schemaFilter) {
511
495
  schemasToSearch = [schemaFilter];
512
496
  } else {
513
- schemasToSearch = await connector.getSchemas();
497
+ schemasToSearch = await resolveDefaultSchemas(connector);
514
498
  }
515
499
  for (const schemaName of schemasToSearch) {
516
500
  if (results.length >= limit) break;
@@ -586,36 +570,6 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
586
570
  for (const schemaName of schemasToSearch) {
587
571
  if (results.length >= limit) break;
588
572
  try {
589
- const searchableConnector = connector;
590
- if (searchableConnector.searchColumns) {
591
- const matchedColumns = await searchableConnector.searchColumns(
592
- pattern,
593
- schemaName,
594
- tableFilter,
595
- limit - results.length
596
- );
597
- for (const column of matchedColumns) {
598
- if (results.length >= limit) break;
599
- if (detailLevel === "names") {
600
- results.push({
601
- name: column.name,
602
- table: column.table,
603
- schema: column.schema
604
- });
605
- } else {
606
- results.push({
607
- name: column.name,
608
- table: column.table,
609
- schema: column.schema,
610
- type: column.type,
611
- nullable: column.nullable,
612
- default: column.default,
613
- ...column.description ? { description: column.description } : {}
614
- });
615
- }
616
- }
617
- continue;
618
- }
619
573
  let tablesToSearch;
620
574
  if (tableFilter) {
621
575
  tablesToSearch = [tableFilter];
@@ -791,10 +745,10 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
791
745
  }
792
746
  }
793
747
  if (schema) {
794
- const exists = connector.schemaExists ? await connector.schemaExists(schema) : (await connector.getSchemas()).includes(schema);
795
- if (!exists) {
748
+ const schemas = await connector.getSchemas();
749
+ if (!schemas.includes(schema)) {
796
750
  success = false;
797
- errorMessage = `Schema '${schema}' does not exist`;
751
+ errorMessage = `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`;
798
752
  return createToolErrorResponse(errorMessage, "SCHEMA_NOT_FOUND");
799
753
  }
800
754
  }
@@ -861,15 +815,151 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
861
815
  };
862
816
  }
863
817
 
864
- // src/utils/tool-metadata.ts
818
+ // src/tools/explain-sql.ts
865
819
  import { z as z3 } from "zod";
820
+ var explainSqlSchema = {
821
+ sql: z3.string().describe("Single SQL statement to explain (no trailing semicolon-separated statements)")
822
+ };
823
+ var explainSqlInputSchema = z3.object(explainSqlSchema);
824
+ function buildExplainStatement(connectorType, sql) {
825
+ if (connectorType === "sqlite") {
826
+ return `EXPLAIN QUERY PLAN ${sql}`;
827
+ }
828
+ return `EXPLAIN ${sql}`;
829
+ }
830
+ function validateExplainInput(sql, connectorType) {
831
+ const statements = splitSQLStatements(sql, connectorType);
832
+ if (statements.length !== 1) {
833
+ return "explain_sql only supports a single SQL statement";
834
+ }
835
+ const firstWord = getFirstKeyword(statements[0], connectorType);
836
+ if (firstWord === "explain") {
837
+ return "explain_sql input must not itself start with EXPLAIN";
838
+ }
839
+ if (firstWord === "analyze") {
840
+ return "explain_sql does not support ANALYZE (it must never execute the statement)";
841
+ }
842
+ const cleaned = stripCommentsAndStrings(statements[0], connectorType).trim();
843
+ const leadingOptions = cleaned.match(/^\(([\s\S]*?)\)/)?.[1];
844
+ if (leadingOptions && /\banalyze\b/i.test(leadingOptions)) {
845
+ return "explain_sql does not support ANALYZE (it must never execute the statement)";
846
+ }
847
+ return null;
848
+ }
849
+ function createExplainSqlToolHandler(sourceId) {
850
+ return async (args, extra) => {
851
+ const { sql } = args;
852
+ const startTime = Date.now();
853
+ const effectiveSourceId = getEffectiveSourceId(sourceId);
854
+ let success = true;
855
+ let errorMessage;
856
+ let result;
857
+ try {
858
+ await ConnectorManager.ensureConnected(sourceId);
859
+ const connector = ConnectorManager.getCurrentConnector(sourceId);
860
+ const validationError = validateExplainInput(sql, connector.id);
861
+ if (validationError) {
862
+ success = false;
863
+ errorMessage = validationError;
864
+ return createToolErrorResponse(errorMessage, "INVALID_INPUT");
865
+ }
866
+ const explainStatement = buildExplainStatement(connector.id, sql);
867
+ result = await connector.executeSQL(explainStatement, { readonly: true });
868
+ const responseData = {
869
+ rows: result.resultSets[0].rows,
870
+ count: result.resultSets[0].rowCount,
871
+ source_id: effectiveSourceId,
872
+ ...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
873
+ };
874
+ return createToolSuccessResponse(responseData);
875
+ } catch (error) {
876
+ success = false;
877
+ errorMessage = error.message;
878
+ const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
879
+ if (classified) return classified;
880
+ return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
881
+ } finally {
882
+ trackToolRequest(
883
+ {
884
+ sourceId: effectiveSourceId,
885
+ toolName: resolveTrackedToolName(sourceId, "explain_sql"),
886
+ sql
887
+ },
888
+ startTime,
889
+ extra,
890
+ success,
891
+ errorMessage
892
+ );
893
+ }
894
+ };
895
+ }
866
896
 
867
- // src/utils/normalize-id.ts
868
- function normalizeSourceId(id) {
869
- return id.replace(/[^a-zA-Z0-9]/g, "_");
897
+ // src/tools/health-check.ts
898
+ import { z as z4 } from "zod";
899
+ var healthCheckSchema = {};
900
+ var healthCheckInputSchema = z4.object(healthCheckSchema);
901
+ function createHealthCheckToolHandler(sourceId) {
902
+ return async (_args, extra) => {
903
+ const startTime = Date.now();
904
+ const effectiveSourceId = getEffectiveSourceId(sourceId);
905
+ let success = true;
906
+ let errorMessage;
907
+ try {
908
+ await ConnectorManager.ensureConnected(sourceId);
909
+ const connector = ConnectorManager.getCurrentConnector(sourceId);
910
+ if (!connector.getHealthCheck) {
911
+ success = false;
912
+ errorMessage = `health_check is not supported for '${connector.id}' sources yet`;
913
+ return createToolErrorResponse(errorMessage, "UNSUPPORTED");
914
+ }
915
+ const health = await connector.getHealthCheck();
916
+ return createToolSuccessResponse({
917
+ source_id: effectiveSourceId,
918
+ ...health.connections ? {
919
+ connections: {
920
+ total: health.connections.total,
921
+ active: health.connections.active,
922
+ idle: health.connections.idle,
923
+ idle_in_transaction: health.connections.idleInTransaction,
924
+ ...health.connections.idleInTransactionAborted !== void 0 ? { idle_in_transaction_aborted: health.connections.idleInTransactionAborted } : {},
925
+ max_connections: health.connections.maxConnections,
926
+ longest_idle_in_transaction_seconds: health.connections.longestIdleInTransactionSeconds,
927
+ longest_active_query_seconds: health.connections.longestActiveQuerySeconds
928
+ }
929
+ } : {},
930
+ ...health.bufferCache ? {
931
+ buffer_cache: {
932
+ hit_ratio_pct: health.bufferCache.hitRatioPct,
933
+ blocks_hit: health.bufferCache.blocksHit,
934
+ blocks_read: health.bufferCache.blocksRead
935
+ }
936
+ } : {},
937
+ ...health.notes && health.notes.length > 0 ? { notes: health.notes } : {}
938
+ });
939
+ } catch (error) {
940
+ success = false;
941
+ errorMessage = error.message;
942
+ const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
943
+ if (classified) return classified;
944
+ return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
945
+ } finally {
946
+ trackToolRequest(
947
+ {
948
+ sourceId: effectiveSourceId,
949
+ toolName: effectiveSourceId === "default" ? "health_check" : `health_check_${effectiveSourceId}`,
950
+ sql: ""
951
+ },
952
+ startTime,
953
+ extra,
954
+ success,
955
+ errorMessage
956
+ );
957
+ }
958
+ };
870
959
  }
871
960
 
872
961
  // src/utils/tool-metadata.ts
962
+ import { z as z5 } from "zod";
873
963
  function buildSourceDescriptionPrefix(description) {
874
964
  const trimmed = description?.trim() ?? "";
875
965
  if (!trimmed) return "";
@@ -879,17 +969,17 @@ function zodToParameters(schema) {
879
969
  const parameters = [];
880
970
  for (const [key, zodType] of Object.entries(schema)) {
881
971
  const description = zodType.description || "";
882
- const required = !(zodType instanceof z3.ZodOptional);
972
+ const required = !(zodType instanceof z5.ZodOptional);
883
973
  let type = "string";
884
- if (zodType instanceof z3.ZodString) {
974
+ if (zodType instanceof z5.ZodString) {
885
975
  type = "string";
886
- } else if (zodType instanceof z3.ZodNumber) {
976
+ } else if (zodType instanceof z5.ZodNumber) {
887
977
  type = "number";
888
- } else if (zodType instanceof z3.ZodBoolean) {
978
+ } else if (zodType instanceof z5.ZodBoolean) {
889
979
  type = "boolean";
890
- } else if (zodType instanceof z3.ZodArray) {
980
+ } else if (zodType instanceof z5.ZodArray) {
891
981
  type = "array";
892
- } else if (zodType instanceof z3.ZodObject) {
982
+ } else if (zodType instanceof z5.ZodObject) {
893
983
  type = "object";
894
984
  }
895
985
  parameters.push({
@@ -901,24 +991,32 @@ function zodToParameters(schema) {
901
991
  }
902
992
  return parameters;
903
993
  }
904
- function getExecuteSqlMetadata(sourceId) {
994
+ function resolveBuiltinToolNaming(sourceId, baseName, titleLabel) {
905
995
  const sourceIds = ConnectorManager.getAvailableSourceIds();
906
996
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
907
997
  const dbType = sourceConfig.type;
908
998
  const isSingleSource = sourceIds.length === 1;
999
+ const name = isSingleSource ? baseName : `${baseName}_${normalizeSourceId(sourceId)}`;
1000
+ const title = isSingleSource ? `${titleLabel} (${dbType})` : `${titleLabel} on ${sourceId} (${dbType})`;
1001
+ const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
1002
+ return { dbType, isSingleSource, name, title, userDescPrefix };
1003
+ }
1004
+ function getExecuteSqlMetadata(sourceId) {
1005
+ const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
1006
+ sourceId,
1007
+ "execute_sql",
1008
+ "Execute SQL"
1009
+ );
909
1010
  const registry = getToolRegistry();
910
1011
  const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
911
1012
  const executeOptions = {
912
1013
  readonly: toolConfig?.readonly,
913
1014
  maxRows: toolConfig?.max_rows
914
1015
  };
915
- const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
916
- const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
917
- const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
918
1016
  const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
919
- const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
1017
+ const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows; capped results carry "truncated": true)` : "";
920
1018
  const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
921
- const isReadonly = executeOptions.readonly === true;
1019
+ const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
922
1020
  const annotations = {
923
1021
  title,
924
1022
  readOnlyHint: isReadonly,
@@ -931,25 +1029,62 @@ function getExecuteSqlMetadata(sourceId) {
931
1029
  openWorldHint: false
932
1030
  };
933
1031
  return {
934
- name: toolName,
1032
+ name,
935
1033
  description,
936
1034
  schema: executeSqlSchema,
937
1035
  annotations
938
1036
  };
939
1037
  }
940
1038
  function getSearchObjectsMetadata(sourceId) {
941
- const sourceIds = ConnectorManager.getAvailableSourceIds();
942
- const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
943
- const dbType = sourceConfig.type;
944
- const isSingleSource = sourceIds.length === 1;
945
- const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
946
- const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
947
- const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
1039
+ const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
1040
+ sourceId,
1041
+ "search_objects",
1042
+ "Search Database Objects"
1043
+ );
948
1044
  const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
1045
+ return { name, description, title };
1046
+ }
1047
+ function getExplainSqlMetadata(sourceId) {
1048
+ const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
1049
+ sourceId,
1050
+ "explain_sql",
1051
+ "Explain Query Plan"
1052
+ );
1053
+ 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).`;
1054
+ const annotations = {
1055
+ title,
1056
+ readOnlyHint: true,
1057
+ destructiveHint: false,
1058
+ idempotentHint: true,
1059
+ openWorldHint: false
1060
+ };
949
1061
  return {
950
- name: toolName,
1062
+ name,
951
1063
  description,
952
- title
1064
+ schema: explainSqlSchema,
1065
+ annotations
1066
+ };
1067
+ }
1068
+ function getHealthCheckMetadata(sourceId) {
1069
+ const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
1070
+ sourceId,
1071
+ "health_check",
1072
+ "Health Check"
1073
+ );
1074
+ 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).`;
1075
+ const annotations = {
1076
+ title,
1077
+ readOnlyHint: true,
1078
+ destructiveHint: false,
1079
+ idempotentHint: false,
1080
+ // Metrics change from call to call, unlike explain_sql's plan output
1081
+ openWorldHint: false
1082
+ };
1083
+ return {
1084
+ name,
1085
+ description,
1086
+ schema: healthCheckSchema,
1087
+ annotations
953
1088
  };
954
1089
  }
955
1090
  function customParamsToToolParams(params) {
@@ -986,13 +1121,13 @@ function buildSearchObjectsTool(sourceId) {
986
1121
  name: "object_type",
987
1122
  type: "string",
988
1123
  required: true,
989
- description: "Object type to search: schema, table, view, column, procedure, function, index"
1124
+ description: "Object type to search"
990
1125
  },
991
1126
  {
992
1127
  name: "pattern",
993
1128
  type: "string",
994
1129
  required: false,
995
- description: "LIKE pattern (% = any chars, _ = one char). Default: %"
1130
+ description: "LIKE pattern (% = any chars, _ = one char)"
996
1131
  },
997
1132
  {
998
1133
  name: "schema",
@@ -1016,13 +1151,23 @@ function buildSearchObjectsTool(sourceId) {
1016
1151
  name: "limit",
1017
1152
  type: "integer",
1018
1153
  required: false,
1019
- description: "Max results (default: 100, max: 1000)"
1154
+ description: "Max results"
1020
1155
  }
1021
1156
  ],
1022
1157
  readonly: true
1023
1158
  // search_objects is always readonly
1024
1159
  };
1025
1160
  }
1161
+ function buildExplainSqlTool(sourceId) {
1162
+ const explainMetadata = getExplainSqlMetadata(sourceId);
1163
+ return {
1164
+ name: explainMetadata.name,
1165
+ description: explainMetadata.description,
1166
+ parameters: zodToParameters(explainMetadata.schema),
1167
+ readonly: true
1168
+ // explain_sql is always readonly
1169
+ };
1170
+ }
1026
1171
  function buildCustomTool(toolConfig) {
1027
1172
  return {
1028
1173
  name: toolConfig.name,
@@ -1041,6 +1186,8 @@ function getToolsForSource(sourceId) {
1041
1186
  return buildExecuteSqlTool(sourceId, toolConfig);
1042
1187
  } else if (toolConfig.name === "search_objects") {
1043
1188
  return buildSearchObjectsTool(sourceId);
1189
+ } else if (toolConfig.name === "explain_sql") {
1190
+ return buildExplainSqlTool(sourceId);
1044
1191
  } else {
1045
1192
  return buildCustomTool(toolConfig);
1046
1193
  }
@@ -1048,7 +1195,7 @@ function getToolsForSource(sourceId) {
1048
1195
  }
1049
1196
 
1050
1197
  // src/tools/custom-tool-handler.ts
1051
- import { z as z4 } from "zod";
1198
+ import { z as z6 } from "zod";
1052
1199
  function buildZodSchemaFromParameters(parameters) {
1053
1200
  if (!parameters || parameters.length === 0) {
1054
1201
  return {};
@@ -1058,26 +1205,26 @@ function buildZodSchemaFromParameters(parameters) {
1058
1205
  let fieldSchema;
1059
1206
  switch (param.type) {
1060
1207
  case "string":
1061
- fieldSchema = z4.string().describe(param.description);
1208
+ fieldSchema = z6.string().describe(param.description);
1062
1209
  break;
1063
1210
  case "integer":
1064
- fieldSchema = z4.number().int().describe(param.description);
1211
+ fieldSchema = z6.number().int().describe(param.description);
1065
1212
  break;
1066
1213
  case "float":
1067
- fieldSchema = z4.number().describe(param.description);
1214
+ fieldSchema = z6.number().describe(param.description);
1068
1215
  break;
1069
1216
  case "boolean":
1070
- fieldSchema = z4.boolean().describe(param.description);
1217
+ fieldSchema = z6.boolean().describe(param.description);
1071
1218
  break;
1072
1219
  case "array":
1073
- fieldSchema = z4.array(z4.unknown()).describe(param.description);
1220
+ fieldSchema = z6.array(z6.unknown()).describe(param.description);
1074
1221
  break;
1075
1222
  default:
1076
1223
  throw new Error(`Unsupported parameter type: ${param.type}`);
1077
1224
  }
1078
1225
  if (param.allowed_values && param.allowed_values.length > 0) {
1079
1226
  if (param.type === "string") {
1080
- fieldSchema = z4.enum(param.allowed_values).describe(param.description);
1227
+ fieldSchema = z6.enum(param.allowed_values).describe(param.description);
1081
1228
  } else {
1082
1229
  fieldSchema = fieldSchema.refine(
1083
1230
  (val) => param.allowed_values.includes(val),
@@ -1094,9 +1241,17 @@ function buildZodSchemaFromParameters(parameters) {
1094
1241
  }
1095
1242
  return schemaShape;
1096
1243
  }
1244
+ var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
1245
+ function getCustomToolInputSchema(toolConfig) {
1246
+ let schema = customToolInputSchemas.get(toolConfig);
1247
+ if (!schema) {
1248
+ schema = z6.object(buildZodSchemaFromParameters(toolConfig.parameters));
1249
+ customToolInputSchemas.set(toolConfig, schema);
1250
+ }
1251
+ return schema;
1252
+ }
1097
1253
  function createCustomToolHandler(toolConfig) {
1098
- const zodSchemaShape = buildZodSchemaFromParameters(toolConfig.parameters);
1099
- const zodSchema = z4.object(zodSchemaShape);
1254
+ const zodSchema = getCustomToolInputSchema(toolConfig);
1100
1255
  return async (args, extra) => {
1101
1256
  const startTime = Date.now();
1102
1257
  let success = true;
@@ -1106,12 +1261,12 @@ function createCustomToolHandler(toolConfig) {
1106
1261
  const validatedArgs = zodSchema.parse(args);
1107
1262
  await ConnectorManager.ensureConnected(toolConfig.source);
1108
1263
  const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
1264
+ const policy = policyFromReadonly(toolConfig.readonly);
1109
1265
  const executeOptions = {
1110
- readonly: toolConfig.readonly,
1266
+ readonly: isReadOnlyPolicy(policy),
1111
1267
  maxRows: toolConfig.max_rows
1112
1268
  };
1113
- const isReadonly = executeOptions.readonly === true;
1114
- if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
1269
+ if (sqlVerdict(policy, toolConfig.statement, connector.id) === "deny") {
1115
1270
  errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
1116
1271
  success = false;
1117
1272
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
@@ -1126,8 +1281,7 @@ function createCustomToolHandler(toolConfig) {
1126
1281
  paramValues
1127
1282
  );
1128
1283
  const responseData = {
1129
- rows: result.rows,
1130
- count: result.rowCount,
1284
+ statements: toStatementsPayload(result.resultSets),
1131
1285
  source_id: toolConfig.source
1132
1286
  };
1133
1287
  return createToolSuccessResponse(responseData);
@@ -1136,7 +1290,7 @@ function createCustomToolHandler(toolConfig) {
1136
1290
  errorMessage = error.message;
1137
1291
  const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
1138
1292
  if (classified) return classified;
1139
- if (error instanceof z4.ZodError) {
1293
+ if (error instanceof z6.ZodError) {
1140
1294
  const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1141
1295
  errorMessage = `Parameter validation failed: ${issues}`;
1142
1296
  } else {
@@ -1176,6 +1330,10 @@ function registerTools(server) {
1176
1330
  registerExecuteSqlTool(server, sourceId);
1177
1331
  } else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
1178
1332
  registerSearchObjectsTool(server, sourceId);
1333
+ } else if (toolConfig.name === BUILTIN_TOOL_EXPLAIN_SQL) {
1334
+ registerExplainSqlTool(server, sourceId);
1335
+ } else if (toolConfig.name === BUILTIN_TOOL_HEALTH_CHECK) {
1336
+ registerHealthCheckTool(server, sourceId);
1179
1337
  } else {
1180
1338
  registerCustomTool(server, sourceId, toolConfig);
1181
1339
  }
@@ -1188,7 +1346,7 @@ function registerExecuteSqlTool(server, sourceId) {
1188
1346
  metadata.name,
1189
1347
  {
1190
1348
  description: metadata.description,
1191
- inputSchema: metadata.schema,
1349
+ inputSchema: executeSqlInputSchema,
1192
1350
  annotations: metadata.annotations
1193
1351
  },
1194
1352
  createExecuteSqlToolHandler(sourceId)
@@ -1200,7 +1358,7 @@ function registerSearchObjectsTool(server, sourceId) {
1200
1358
  metadata.name,
1201
1359
  {
1202
1360
  description: metadata.description,
1203
- inputSchema: searchDatabaseObjectsSchema,
1361
+ inputSchema: searchDatabaseObjectsInputSchema,
1204
1362
  annotations: {
1205
1363
  title: metadata.title,
1206
1364
  readOnlyHint: true,
@@ -1212,16 +1370,39 @@ function registerSearchObjectsTool(server, sourceId) {
1212
1370
  createSearchDatabaseObjectsToolHandler(sourceId)
1213
1371
  );
1214
1372
  }
1373
+ function registerExplainSqlTool(server, sourceId) {
1374
+ const metadata = getExplainSqlMetadata(sourceId);
1375
+ server.registerTool(
1376
+ metadata.name,
1377
+ {
1378
+ description: metadata.description,
1379
+ inputSchema: explainSqlInputSchema,
1380
+ annotations: metadata.annotations
1381
+ },
1382
+ createExplainSqlToolHandler(sourceId)
1383
+ );
1384
+ }
1385
+ function registerHealthCheckTool(server, sourceId) {
1386
+ const metadata = getHealthCheckMetadata(sourceId);
1387
+ server.registerTool(
1388
+ metadata.name,
1389
+ {
1390
+ description: metadata.description,
1391
+ inputSchema: healthCheckInputSchema,
1392
+ annotations: metadata.annotations
1393
+ },
1394
+ createHealthCheckToolHandler(sourceId)
1395
+ );
1396
+ }
1215
1397
  function registerCustomTool(server, sourceId, toolConfig) {
1216
1398
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
1217
1399
  const dbType = sourceConfig.type;
1218
- const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
1219
- const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
1400
+ const isReadOnly = classifySQL(toolConfig.statement, dbType) === "read";
1220
1401
  server.registerTool(
1221
1402
  toolConfig.name,
1222
1403
  {
1223
1404
  description: toolConfig.description,
1224
- inputSchema: zodSchema,
1405
+ inputSchema: getCustomToolInputSchema(toolConfig),
1225
1406
  annotations: {
1226
1407
  title: `${toolConfig.name} (${dbType})`,
1227
1408
  readOnlyHint: isReadOnly,
@@ -1654,6 +1835,32 @@ function validateOrigin(originHeader, hostHeader, allowedHosts) {
1654
1835
  return { ok: true };
1655
1836
  }
1656
1837
 
1838
+ // src/utils/auth-token.ts
1839
+ import { timingSafeEqual } from "node:crypto";
1840
+ var BEARER_PREFIX = "Bearer ";
1841
+ function constantTimeEqual(a, b) {
1842
+ const bufA = Buffer.from(a);
1843
+ const bufB = Buffer.from(b);
1844
+ if (bufA.length !== bufB.length) return false;
1845
+ return timingSafeEqual(bufA, bufB);
1846
+ }
1847
+ function validateAuthToken(authorizationHeader, tokens) {
1848
+ if (tokens.length === 0) return { ok: true };
1849
+ if (!authorizationHeader || !authorizationHeader.startsWith(BEARER_PREFIX)) {
1850
+ return {
1851
+ ok: false,
1852
+ status: 401,
1853
+ message: "Missing or malformed Authorization header. Expected: Bearer <token>"
1854
+ };
1855
+ }
1856
+ const presented = authorizationHeader.slice(BEARER_PREFIX.length);
1857
+ const matches = tokens.some((token) => constantTimeEqual(presented, token));
1858
+ if (!matches) {
1859
+ return { ok: false, status: 401, message: "Invalid bearer token" };
1860
+ }
1861
+ return { ok: true };
1862
+ }
1863
+
1657
1864
  // src/server.ts
1658
1865
  var __filename = fileURLToPath(import.meta.url);
1659
1866
  var __dirname = path.dirname(__filename);
@@ -1661,6 +1868,8 @@ var packageJsonPath = path.join(__dirname, "..", "package.json");
1661
1868
  var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1662
1869
  var SERVER_NAME = "DBHub MCP Server";
1663
1870
  var SERVER_VERSION = packageJson.version;
1871
+ var TOOLS_LIST_CACHE_TTL_MS = 3e5;
1872
+ var logMcpError = (scope) => (error) => console.error(`MCP ${scope} error:`, error);
1664
1873
  function generateBanner(version, modes = []) {
1665
1874
  const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
1666
1875
  return `
@@ -1706,7 +1915,7 @@ See documentation for more details on configuring database connections.
1706
1915
  const sources = sourceConfigsData.sources;
1707
1916
  console.error(`Configuration source: ${sourceConfigsData.source}`);
1708
1917
  await connectorManager.connectWithSources(sources);
1709
- const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-XSX2VZTD.js");
1918
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-WVSFVS35.js");
1710
1919
  initializeToolRegistry2({
1711
1920
  sources: sourceConfigsData.sources,
1712
1921
  tools: sourceConfigsData.tools
@@ -1717,10 +1926,18 @@ See documentation for more details on configuring database connections.
1717
1926
  initialTools: sourceConfigsData.tools
1718
1927
  });
1719
1928
  const createServer = () => {
1720
- const server = new McpServer({
1721
- name: SERVER_NAME,
1722
- version: SERVER_VERSION
1723
- });
1929
+ const server = new McpServer(
1930
+ {
1931
+ name: SERVER_NAME,
1932
+ version: SERVER_VERSION
1933
+ },
1934
+ {
1935
+ // `private` keeps caching client-side (no shared/proxy caches).
1936
+ cacheHints: {
1937
+ "tools/list": { ttlMs: TOOLS_LIST_CACHE_TTL_MS, cacheScope: "private" }
1938
+ }
1939
+ }
1940
+ );
1724
1941
  registerTools(server);
1725
1942
  return server;
1726
1943
  };
@@ -1728,6 +1945,7 @@ See documentation for more details on configuring database connections.
1728
1945
  const port = transportData.type === "http" ? resolvePort().port : null;
1729
1946
  const host = transportData.type === "http" ? resolveHost().host : null;
1730
1947
  const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
1948
+ const { tokens: authTokens, source: authTokenSource } = resolveAuthTokens();
1731
1949
  const activeModes = [];
1732
1950
  const modeDescriptions = [];
1733
1951
  const isDemo = isDemoMode();
@@ -1762,44 +1980,33 @@ See documentation for more details on configuring database connections.
1762
1980
  }
1763
1981
  res.header("Access-Control-Allow-Origin", origin || "http://localhost");
1764
1982
  res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1765
- res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
1983
+ res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
1766
1984
  res.header("Access-Control-Allow-Credentials", "true");
1767
1985
  if (req.method === "OPTIONS") {
1768
1986
  return res.sendStatus(200);
1769
1987
  }
1770
1988
  next();
1771
1989
  });
1772
- const frontendPath = path.join(__dirname, "public");
1773
- app.use(express.static(frontendPath));
1774
1990
  app.get("/healthz", (req, res) => {
1775
1991
  res.status(200).send("OK");
1776
1992
  });
1993
+ app.use((req, res, next) => {
1994
+ const result = validateAuthToken(req.headers.authorization, authTokens);
1995
+ if (!result.ok) {
1996
+ res.header("WWW-Authenticate", "Bearer");
1997
+ return res.status(result.status).json({ error: "Unauthorized", message: result.message });
1998
+ }
1999
+ next();
2000
+ });
2001
+ const frontendPath = path.join(__dirname, "public");
2002
+ app.use(express.static(frontendPath));
1777
2003
  app.get("/api/sources", listSources);
1778
2004
  app.get("/api/sources/:sourceId", getSource);
1779
2005
  app.get("/api/requests", listRequests);
1780
- app.get("/mcp", (req, res) => {
1781
- res.status(405).json({
1782
- error: "Method Not Allowed",
1783
- message: "SSE streaming is not supported in stateless mode. Use POST requests with JSON responses."
1784
- });
1785
- });
1786
- app.post("/mcp", async (req, res) => {
1787
- try {
1788
- const transport = new StreamableHTTPServerTransport({
1789
- sessionIdGenerator: void 0,
1790
- // Disable session management for stateless mode
1791
- enableJsonResponse: true
1792
- // Use JSON responses (SSE not supported in stateless mode)
1793
- });
1794
- const server = createServer();
1795
- await server.connect(transport);
1796
- await transport.handleRequest(req, res, req.body);
1797
- } catch (error) {
1798
- console.error("Error handling request:", error);
1799
- if (!res.headersSent) {
1800
- res.status(500).json({ error: "Internal server error" });
1801
- }
1802
- }
2006
+ const mcpHandler = createMcpHandler(createServer, { onerror: logMcpError("handler") });
2007
+ const mcpNodeHandler = toNodeHandler(mcpHandler, { onerror: logMcpError("adapter") });
2008
+ app.all("/mcp", (req, res) => {
2009
+ void mcpNodeHandler(req, res, req.body);
1803
2010
  });
1804
2011
  if (process.env.NODE_ENV !== "development") {
1805
2012
  app.get("*", (req, res) => {
@@ -1824,6 +2031,11 @@ See documentation for more details on configuring database connections.
1824
2031
  } else {
1825
2032
  console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
1826
2033
  }
2034
+ if (authTokens.length > 0) {
2035
+ console.error(`Auth: bearer token required (${authTokens.length} token(s) configured via ${authTokenSource})`);
2036
+ } else {
2037
+ console.error("Auth: disabled (set --auth-token or DBHUB_AUTH_TOKEN to require a bearer token)");
2038
+ }
1827
2039
  if (process.env.NODE_ENV === "development") {
1828
2040
  console.error("Development mode detected!");
1829
2041
  console.error(" Workbench dev server (with HMR): http://localhost:5173");
@@ -1835,16 +2047,14 @@ See documentation for more details on configuring database connections.
1835
2047
  console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
1836
2048
  });
1837
2049
  } else {
1838
- const server = createServer();
1839
- const transport = new StdioServerTransport();
1840
- await server.connect(transport);
2050
+ const stdioHandle = serveStdio(createServer, { onerror: logMcpError("stdio") });
1841
2051
  console.error("MCP server running on stdio");
1842
2052
  let isShuttingDown = false;
1843
2053
  const shutdown = async () => {
1844
2054
  if (isShuttingDown) return;
1845
2055
  isShuttingDown = true;
1846
2056
  console.error("Shutting down...");
1847
- await transport.close();
2057
+ await stdioHandle.close();
1848
2058
  await connectorManager.disconnect();
1849
2059
  process.exit(0);
1850
2060
  };
@@ -1860,12 +2070,12 @@ See documentation for more details on configuring database connections.
1860
2070
 
1861
2071
  // src/index.ts
1862
2072
  var connectorModules = [
1863
- { load: () => import("./postgres-VFNLBWMF.js"), name: "PostgreSQL", driver: "pg" },
1864
- { load: () => import("./sqlserver-OE4SFH4B.js"), name: "SQL Server", driver: "mssql" },
1865
- { load: () => import("./sqlite-IOUAYHGE.js"), name: "SQLite", driver: "node:sqlite" },
1866
- { load: () => import("./mysql-A43SL7UM.js"), name: "MySQL", driver: "mysql2" },
1867
- { load: () => import("./mariadb-7F72IRB4.js"), name: "MariaDB", driver: "mariadb" },
1868
- { load: () => import("./dameng-5KYI5JBK.js"), name: "Dameng", driver: "dmdb" }
2073
+ { load: () => import("./postgres-C24SBQ4I.js"), name: "PostgreSQL", driver: "pg" },
2074
+ { load: () => import("./sqlserver-KZPPK67Q.js"), name: "SQL Server", driver: "mssql" },
2075
+ { load: () => import("./dameng-NKF7NZKE.js"), name: "Dameng", driver: "dmdb" },
2076
+ { load: () => import("./sqlite-7BNGDMBZ.js"), name: "SQLite", driver: "node:sqlite" },
2077
+ { load: () => import("./mysql-TESH7KKV.js"), name: "MySQL", driver: "mysql2" },
2078
+ { load: () => import("./mariadb-MMC6WMEG.js"), name: "MariaDB", driver: "mariadb" }
1869
2079
  ];
1870
2080
  loadConnectors(connectorModules).then(() => main()).catch((error) => {
1871
2081
  console.error("Fatal error:", error);