UNPKG

@bytebase/dbhub

Version:

Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite

2,082 lines • 71.1 kB
#!/usr/bin/env node
import {
  BUILTIN_TOOL_EXECUTE_SQL,
  BUILTIN_TOOL_EXPLAIN_SQL,
  BUILTIN_TOOL_HEALTH_CHECK,
  BUILTIN_TOOL_SEARCH_OBJECTS,
  ConnectorManager,
  classifyConnectionError,
  getToolRegistry,
  initializeToolRegistry,
  isDemoMode,
  loadTomlConfig,
  mapArgumentsToArray,
  resolveAllowedHosts,
  resolveAuthTokens,
  resolveHost,
  resolvePort,
  resolveSourceConfigs,
  resolveTomlConfigPath,
  resolveTransport
} from "./chunk-MJVIE3QY.js";
import {
  loadConnectors
} from "./chunk-WVVMH6FJ.js";
import {
  quoteQualifiedIdentifier
} from "./chunk-X2EC47W4.js";
import {
  allowedKeywords,
  getFirstKeyword,
  hasEscapeHatchFunction,
  isReadOnlySQL,
  sqlServerDynamicSqlPattern
} from "./chunk-ABTVMHD6.js";
import {
  ConnectorRegistry,
  getDatabaseTypeFromDSN,
  getDefaultPortForType,
  parseConnectionInfoFromDSN,
  splitSQLStatements,
  stripCommentsAndStrings
} from "./chunk-IXC4NQGC.js";

// src/server.ts
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { toNodeHandler } from "@modelcontextprotocol/node";
import express from "express";
import http from "http";
import path from "path";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";

// src/tools/execute-sql.ts
import { z } from "zod";

// src/utils/response-formatter.ts
var MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);
var MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
function bigIntReplacer(_key, value) {
  if (typeof value === "bigint") {
    if (value >= MIN_SAFE_BIGINT && value <= MAX_SAFE_BIGINT) {
      return Number(value);
    }
    return value.toString();
  }
  return value;
}
function formatSuccessResponse(data, meta = {}) {
  return {
    success: true,
    data,
    ...Object.keys(meta).length > 0 ? { meta } : {}
  };
}
function formatErrorResponse(error, code = "ERROR", details) {
  return {
    success: false,
    error,
    code,
    ...details ? { details } : {}
  };
}
function createToolErrorResponse(error, code = "ERROR", details) {
  return {
    content: [
      {
        type: "text",
        text: JSON.stringify(formatErrorResponse(error, code, details), bigIntReplacer, 2),
        mimeType: "application/json"
      }
    ],
    isError: true
  };
}
function createToolSuccessResponse(data, meta = {}) {
  return {
    content: [
      {
        type: "text",
        text: JSON.stringify(formatSuccessResponse(data, meta), bigIntReplacer, 2),
        mimeType: "application/json"
      }
    ]
  };
}

// src/utils/sql-access-policy.ts
var CLASS_SEVERITY = {
  read: 0,
  dml: 1,
  ddl: 2,
  admin: 3,
  unknown: 4
};
var ddlPattern = /\b(?:create|alter|drop|truncate|rename)\b/i;
var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|delayed)\s+)?into)\b/i;
var grantRevokePattern = /\b(?:grant|revoke)\b/i;
function isAdminStatement(stripped, connectorType) {
  if (grantRevokePattern.test(stripped)) return true;
  if (hasEscapeHatchFunction(stripped, connectorType)) return true;
  return connectorType === "sqlserver" && sqlServerDynamicSqlPattern.test(stripped);
}
function policyFromReadonly(readonly) {
  const write = readonly === true ? "deny" : "allow";
  return { read: "allow", dml: write, ddl: write, admin: write, unknown: write };
}
function isReadOnlyPolicy(policy) {
  return policy.dml === "deny" && policy.ddl === "deny" && policy.admin === "deny" && policy.unknown === "deny";
}
function classifyStatement(sql, connectorType) {
  if (isReadOnlySQL(sql, connectorType)) {
    return "read";
  }
  const stripped = stripCommentsAndStrings(sql, connectorType);
  if (isAdminStatement(stripped, connectorType)) return "admin";
  if (ddlPattern.test(stripped)) return "ddl";
  if (dmlPattern.test(stripped)) return "dml";
  return "unknown";
}
function classifySQL(sql, connectorType) {
  let strictest = "read";
  for (const statement of splitSQLStatements(sql, connectorType)) {
    const cls = classifyStatement(statement, connectorType);
    if (CLASS_SEVERITY[cls] > CLASS_SEVERITY[strictest]) {
      strictest = cls;
    }
  }
  return strictest;
}
function sqlVerdict(policy, sql, connectorType) {
  return policy[classifySQL(sql, connectorType)];
}

// src/requests/store.ts
var RequestStore = class {
  constructor() {
    this.store = /* @__PURE__ */ new Map();
    this.maxPerSource = 100;
  }
  /**
   * Add a request to the store
   * Evicts oldest entry if at capacity
   */
  add(request) {
    const requests = this.store.get(request.sourceId) ?? [];
    requests.push(request);
    if (requests.length > this.maxPerSource) {
      requests.shift();
    }
    this.store.set(request.sourceId, requests);
  }
  /**
   * Get requests, optionally filtered by source
   * Returns newest first
   */
  getAll(sourceId) {
    let requests;
    if (sourceId) {
      requests = [...this.store.get(sourceId) ?? []];
    } else {
      requests = Array.from(this.store.values()).flat();
    }
    return requests.sort(
      (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
    );
  }
  /**
   * Get total count of requests across all sources
   */
  getTotal() {
    return Array.from(this.store.values()).reduce((sum, arr) => sum + arr.length, 0);
  }
  /**
   * Clear all requests (useful for testing)
   */
  clear() {
    this.store.clear();
  }
};

// src/requests/index.ts
var requestStore = new RequestStore();

// src/utils/client-identifier.ts
function getClientIdentifier(ctx) {
  const userAgent = ctx?.http?.req?.headers.get("user-agent");
  if (userAgent) {
    return userAgent;
  }
  return "stdio";
}

// src/utils/normalize-id.ts
function normalizeSourceId(id) {
  return id.replace(/[^a-zA-Z0-9]/g, "_");
}

// src/utils/tool-handler-helpers.ts
function getEffectiveSourceId(sourceId) {
  return sourceId || "default";
}
function toStatementsPayload(resultSets) {
  return resultSets.map((set) => ({
    sql: set.sql,
    rows: set.rows,
    count: set.rowCount,
    // Only present when a max_rows cap actually cut off rows, so consumers
    // can tell a capped result from a table with exactly max_rows rows
    // (and complete results don't pay the extra tokens).
    ...set.truncated ? { truncated: true } : {}
  }));
}
function resolveTrackedToolName(sourceId, baseName) {
  const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
  if (isSingleSource) {
    return baseName;
  }
  return `${baseName}_${normalizeSourceId(getEffectiveSourceId(sourceId))}`;
}
function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
  return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
}
function trackToolRequest(metadata, startTime, extra, success, error) {
  requestStore.add({
    id: crypto.randomUUID(),
    timestamp: (/* @__PURE__ */ new Date()).toISOString(),
    sourceId: metadata.sourceId,
    toolName: metadata.toolName,
    sql: metadata.sql,
    durationMs: Date.now() - startTime,
    client: getClientIdentifier(extra),
    success,
    error
  });
}
function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
  let connectorType;
  try {
    connectorType = ConnectorManager.getSourceConfig(rawSourceId)?.type;
  } catch {
    return null;
  }
  if (!connectorType) return null;
  const classified = classifyConnectionError(error, connectorType, displaySourceId);
  if (!classified) return null;
  return createToolErrorResponse(classified.message, classified.code, {
    source_id: displaySourceId
  });
}

// src/tools/execute-sql.ts
var executeSqlSchema = {
  sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
};
var executeSqlInputSchema = z.object(executeSqlSchema);
function createExecuteSqlToolHandler(sourceId) {
  return async (args, extra) => {
    const { sql } = args;
    const startTime = Date.now();
    const effectiveSourceId = getEffectiveSourceId(sourceId);
    let success = true;
    let errorMessage;
    let result;
    try {
      await ConnectorManager.ensureConnected(sourceId);
      const connector = ConnectorManager.getCurrentConnector(sourceId);
      const actualSourceId = connector.getId();
      const registry = getToolRegistry();
      const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
      const policy = policyFromReadonly(toolConfig?.readonly);
      if (sqlVerdict(policy, sql, connector.id) === "deny") {
        errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
        success = false;
        return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
      }
      const executeOptions = {
        readonly: isReadOnlyPolicy(policy),
        maxRows: toolConfig?.max_rows
      };
      result = await connector.executeSQL(sql, executeOptions);
      const responseData = {
        statements: toStatementsPayload(result.resultSets),
        source_id: effectiveSourceId,
        ...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
      };
      return createToolSuccessResponse(responseData);
    } catch (error) {
      success = false;
      errorMessage = error.message;
      const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
      if (classified) return classified;
      return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
    } finally {
      trackToolRequest(
        {
          sourceId: effectiveSourceId,
          toolName: effectiveSourceId === "default" ? "execute_sql" : `execute_sql_${effectiveSourceId}`,
          sql
        },
        startTime,
        extra,
        success,
        errorMessage
      );
    }
  };
}

// src/tools/search-objects.ts
import { z as z2 } from "zod";
var searchDatabaseObjectsSchema = {
  object_type: z2.enum(["schema", "table", "view", "column", "procedure", "function", "index"]).describe("Object type to search"),
  pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char)"),
  schema: z2.string().optional().describe("Filter to schema"),
  table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
  detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
  limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
};
var searchDatabaseObjectsInputSchema = z2.object(searchDatabaseObjectsSchema);
function likePatternToRegex(pattern) {
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
  return new RegExp(`^${escaped}$`, "i");
}
async function resolveDefaultSchemas(connector) {
  if (connector.getDefaultSchema) {
    const defaultSchema = await connector.getDefaultSchema();
    if (defaultSchema) {
      return [defaultSchema];
    }
  }
  return connector.getSchemas();
}
async function getTableRowCount(connector, tableName, schemaName) {
  try {
    if (connector.getTableRowCount) {
      return await connector.getTableRowCount(tableName, schemaName);
    }
    const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
    const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
    const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
    const rows = result.resultSets[0]?.rows;
    if (rows && rows.length > 0) {
      return Number(rows[0].count || rows[0].COUNT || 0);
    }
  } catch (error) {
    return null;
  }
  return null;
}
async function getTableComment(connector, tableName, schemaName) {
  try {
    if (connector.getTableComment) {
      return await connector.getTableComment(tableName, schemaName);
    }
    return null;
  } catch (error) {
    return null;
  }
}
async function searchSchemas(connector, pattern, detailLevel, limit) {
  const schemas = await resolveDefaultSchemas(connector);
  const regex = likePatternToRegex(pattern);
  const matched = schemas.filter((schema) => regex.test(schema)).slice(0, limit);
  if (detailLevel === "names") {
    return matched.map((name) => ({ name }));
  }
  const results = await Promise.all(
    matched.map(async (schemaName) => {
      try {
        const tables = await connector.getTables(schemaName);
        return {
          name: schemaName,
          table_count: tables.length
        };
      } catch (error) {
        return {
          name: schemaName,
          table_count: 0
        };
      }
    })
  );
  return results;
}
async function searchTables(connector, pattern, schemaFilter, detailLevel, limit) {
  const regex = likePatternToRegex(pattern);
  const results = [];
  let schemasToSearch;
  if (schemaFilter) {
    schemasToSearch = [schemaFilter];
  } else {
    schemasToSearch = await resolveDefaultSchemas(connector);
  }
  for (const schemaName of schemasToSearch) {
    if (results.length >= limit) break;
    try {
      const tables = await connector.getTables(schemaName);
      const matched = tables.filter((table) => regex.test(table));
      for (const tableName of matched) {
        if (results.length >= limit) break;
        if (detailLevel === "names") {
          results.push({
            name: tableName,
            schema: schemaName
          });
        } else if (detailLevel === "summary") {
          try {
            const columns = await connector.getTableSchema(tableName, schemaName);
            const rowCount = await getTableRowCount(connector, tableName, schemaName);
            const comment = await getTableComment(connector, tableName, schemaName);
            results.push({
              name: tableName,
              schema: schemaName,
              column_count: columns.length,
              row_count: rowCount,
              ...comment ? { comment } : {}
            });
          } catch (error) {
            results.push({
              name: tableName,
              schema: schemaName,
              column_count: null,
              row_count: null
            });
          }
        } else {
          try {
            const columns = await connector.getTableSchema(tableName, schemaName);
            const indexes = await connector.getTableIndexes(tableName, schemaName);
            const rowCount = await getTableRowCount(connector, tableName, schemaName);
            const comment = await getTableComment(connector, tableName, schemaName);
            results.push({
              name: tableName,
              schema: schemaName,
              column_count: columns.length,
              row_count: rowCount,
              ...comment ? { comment } : {},
              columns: columns.map((col) => ({
                name: col.column_name,
                type: col.data_type,
                nullable: col.is_nullable === "YES",
                default: col.column_default,
                ...col.description ? { description: col.description } : {}
              })),
              indexes: indexes.map((idx) => ({
                name: idx.index_name,
                columns: idx.column_names,
                unique: idx.is_unique,
                primary: idx.is_primary
              }))
            });
          } catch (error) {
            results.push({
              name: tableName,
              schema: schemaName,
              error: `Unable to fetch full details: ${error.message}`
            });
          }
        }
      }
    } catch (error) {
      continue;
    }
  }
  return results;
}
async function searchViews(connector, pattern, schemaFilter, detailLevel, limit) {
  const regex = likePatternToRegex(pattern);
  const results = [];
  let schemasToSearch;
  if (schemaFilter) {
    schemasToSearch = [schemaFilter];
  } else {
    schemasToSearch = await resolveDefaultSchemas(connector);
  }
  for (const schemaName of schemasToSearch) {
    if (results.length >= limit) break;
    try {
      const views = await connector.getViews(schemaName);
      const matched = views.filter((view) => regex.test(view));
      for (const viewName of matched) {
        if (results.length >= limit) break;
        if (detailLevel === "names") {
          results.push({
            name: viewName,
            schema: schemaName
          });
        } else if (detailLevel === "summary") {
          try {
            const columns = await connector.getTableSchema(viewName, schemaName);
            const comment = await getTableComment(connector, viewName, schemaName);
            results.push({
              name: viewName,
              schema: schemaName,
              column_count: columns.length,
              ...comment ? { comment } : {}
            });
          } catch (error) {
            results.push({
              name: viewName,
              schema: schemaName,
              column_count: null
            });
          }
        } else {
          try {
            const columns = await connector.getTableSchema(viewName, schemaName);
            const comment = await getTableComment(connector, viewName, schemaName);
            results.push({
              name: viewName,
              schema: schemaName,
              column_count: columns.length,
              ...comment ? { comment } : {},
              columns: columns.map((col) => ({
                name: col.column_name,
                type: col.data_type,
                nullable: col.is_nullable === "YES",
                default: col.column_default,
                ...col.description ? { description: col.description } : {}
              })),
              indexes: []
            });
          } catch (error) {
            results.push({
              name: viewName,
              schema: schemaName,
              error: `Unable to fetch full details: ${error.message}`
            });
          }
        }
      }
    } catch (error) {
      continue;
    }
  }
  return results;
}
async function searchColumns(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
  const regex = likePatternToRegex(pattern);
  const results = [];
  let schemasToSearch;
  if (schemaFilter) {
    schemasToSearch = [schemaFilter];
  } else {
    schemasToSearch = await resolveDefaultSchemas(connector);
  }
  for (const schemaName of schemasToSearch) {
    if (results.length >= limit) break;
    try {
      let tablesToSearch;
      if (tableFilter) {
        tablesToSearch = [tableFilter];
      } else {
        const [tables, views] = await Promise.all([
          connector.getTables(schemaName),
          Promise.resolve(connector.getViews(schemaName)).catch(() => [])
        ]);
        tablesToSearch = [...tables, ...views ?? []];
      }
      for (const tableName of tablesToSearch) {
        if (results.length >= limit) break;
        try {
          const columns = await connector.getTableSchema(tableName, schemaName);
          const matchedColumns = columns.filter((col) => regex.test(col.column_name));
          for (const column of matchedColumns) {
            if (results.length >= limit) break;
            if (detailLevel === "names") {
              results.push({
                name: column.column_name,
                table: tableName,
                schema: schemaName
              });
            } else {
              results.push({
                name: column.column_name,
                table: tableName,
                schema: schemaName,
                type: column.data_type,
                nullable: column.is_nullable === "YES",
                default: column.column_default,
                ...column.description ? { description: column.description } : {}
              });
            }
          }
        } catch (error) {
          continue;
        }
      }
    } catch (error) {
      continue;
    }
  }
  return results;
}
async function searchProcedures(connector, pattern, schemaFilter, detailLevel, limit, routineType) {
  const regex = likePatternToRegex(pattern);
  const results = [];
  let schemasToSearch;
  if (schemaFilter) {
    schemasToSearch = [schemaFilter];
  } else {
    schemasToSearch = await resolveDefaultSchemas(connector);
  }
  for (const schemaName of schemasToSearch) {
    if (results.length >= limit) break;
    try {
      const procedures = await connector.getStoredProcedures(schemaName, routineType);
      const matched = procedures.filter((proc) => regex.test(proc));
      for (const procName of matched) {
        if (results.length >= limit) break;
        if (detailLevel === "names") {
          results.push({
            name: procName,
            schema: schemaName
          });
        } else {
          try {
            const details = await connector.getStoredProcedureDetail(procName, schemaName);
            results.push({
              name: procName,
              schema: schemaName,
              type: details.procedure_type,
              language: details.language,
              parameters: detailLevel === "full" ? details.parameter_list : void 0,
              return_type: details.return_type,
              definition: detailLevel === "full" ? details.definition : void 0
            });
          } catch (error) {
            results.push({
              name: procName,
              schema: schemaName,
              error: `Unable to fetch details: ${error.message}`
            });
          }
        }
      }
    } catch (error) {
      continue;
    }
  }
  return results;
}
async function searchIndexes(connector, pattern, schemaFilter, tableFilter, detailLevel, limit) {
  const regex = likePatternToRegex(pattern);
  const results = [];
  let schemasToSearch;
  if (schemaFilter) {
    schemasToSearch = [schemaFilter];
  } else {
    schemasToSearch = await resolveDefaultSchemas(connector);
  }
  for (const schemaName of schemasToSearch) {
    if (results.length >= limit) break;
    try {
      let tablesToSearch;
      if (tableFilter) {
        tablesToSearch = [tableFilter];
      } else {
        tablesToSearch = await connector.getTables(schemaName);
      }
      for (const tableName of tablesToSearch) {
        if (results.length >= limit) break;
        try {
          const indexes = await connector.getTableIndexes(tableName, schemaName);
          const matchedIndexes = indexes.filter((idx) => regex.test(idx.index_name));
          for (const index of matchedIndexes) {
            if (results.length >= limit) break;
            if (detailLevel === "names") {
              results.push({
                name: index.index_name,
                table: tableName,
                schema: schemaName
              });
            } else {
              results.push({
                name: index.index_name,
                table: tableName,
                schema: schemaName,
                columns: index.column_names,
                unique: index.is_unique,
                primary: index.is_primary
              });
            }
          }
        } catch (error) {
          continue;
        }
      }
    } catch (error) {
      continue;
    }
  }
  return results;
}
function createSearchDatabaseObjectsToolHandler(sourceId) {
  return async (args, extra) => {
    const {
      object_type,
      pattern = "%",
      schema,
      table,
      detail_level = "names",
      limit = 100
    } = args;
    const startTime = Date.now();
    const effectiveSourceId = getEffectiveSourceId(sourceId);
    let success = true;
    let errorMessage;
    try {
      await ConnectorManager.ensureConnected(sourceId);
      const connector = ConnectorManager.getCurrentConnector(sourceId);
      if (table) {
        if (!schema) {
          success = false;
          errorMessage = "The 'table' parameter requires 'schema' to be specified";
          return createToolErrorResponse(errorMessage, "SCHEMA_REQUIRED");
        }
        if (!["column", "index"].includes(object_type)) {
          success = false;
          errorMessage = `The 'table' parameter only applies to object_type 'column' or 'index', not '${object_type}'`;
          return createToolErrorResponse(errorMessage, "INVALID_TABLE_FILTER");
        }
      }
      if (schema) {
        const schemas = await connector.getSchemas();
        if (!schemas.includes(schema)) {
          success = false;
          errorMessage = `Schema '${schema}' does not exist. Available schemas: ${schemas.join(", ")}`;
          return createToolErrorResponse(errorMessage, "SCHEMA_NOT_FOUND");
        }
      }
      let results = [];
      switch (object_type) {
        case "schema":
          results = await searchSchemas(connector, pattern, detail_level, limit);
          break;
        case "table":
          results = await searchTables(connector, pattern, schema, detail_level, limit);
          break;
        case "view":
          results = await searchViews(connector, pattern, schema, detail_level, limit);
          break;
        case "column":
          results = await searchColumns(connector, pattern, schema, table, detail_level, limit);
          break;
        case "procedure":
          results = await searchProcedures(connector, pattern, schema, detail_level, limit, "procedure");
          break;
        case "function":
          results = await searchProcedures(connector, pattern, schema, detail_level, limit, "function");
          break;
        case "index":
          results = await searchIndexes(connector, pattern, schema, table, detail_level, limit);
          break;
        default:
          success = false;
          errorMessage = `Unsupported object_type: ${object_type}`;
          return createToolErrorResponse(errorMessage, "INVALID_OBJECT_TYPE");
      }
      return createToolSuccessResponse({
        object_type,
        pattern,
        schema,
        table,
        detail_level,
        count: results.length,
        results,
        truncated: results.length === limit
      });
    } catch (error) {
      success = false;
      errorMessage = error.message;
      const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
      if (classified) return classified;
      return createToolErrorResponse(
        `Error searching database objects: ${errorMessage}`,
        "SEARCH_ERROR"
      );
    } finally {
      trackToolRequest(
        {
          sourceId: effectiveSourceId,
          toolName: effectiveSourceId === "default" ? "search_objects" : `search_objects_${effectiveSourceId}`,
          sql: `search_objects(object_type=${object_type}, pattern=${pattern}, schema=${schema || "all"}, table=${table || "all"}, detail_level=${detail_level})`
        },
        startTime,
        extra,
        success,
        errorMessage
      );
    }
  };
}

// src/tools/explain-sql.ts
import { z as z3 } from "zod";
var explainSqlSchema = {
  sql: z3.string().describe("Single SQL statement to explain (no trailing semicolon-separated statements)")
};
var explainSqlInputSchema = z3.object(explainSqlSchema);
function buildExplainStatement(connectorType, sql) {
  if (connectorType === "sqlite") {
    return `EXPLAIN QUERY PLAN ${sql}`;
  }
  return `EXPLAIN ${sql}`;
}
function validateExplainInput(sql, connectorType) {
  const statements = splitSQLStatements(sql, connectorType);
  if (statements.length !== 1) {
    return "explain_sql only supports a single SQL statement";
  }
  const firstWord = getFirstKeyword(statements[0], connectorType);
  if (firstWord === "explain") {
    return "explain_sql input must not itself start with EXPLAIN";
  }
  if (firstWord === "analyze") {
    return "explain_sql does not support ANALYZE (it must never execute the statement)";
  }
  const cleaned = stripCommentsAndStrings(statements[0], connectorType).trim();
  const leadingOptions = cleaned.match(/^\(([\s\S]*?)\)/)?.[1];
  if (leadingOptions && /\banalyze\b/i.test(leadingOptions)) {
    return "explain_sql does not support ANALYZE (it must never execute the statement)";
  }
  return null;
}
function createExplainSqlToolHandler(sourceId) {
  return async (args, extra) => {
    const { sql } = args;
    const startTime = Date.now();
    const effectiveSourceId = getEffectiveSourceId(sourceId);
    let success = true;
    let errorMessage;
    let result;
    try {
      await ConnectorManager.ensureConnected(sourceId);
      const connector = ConnectorManager.getCurrentConnector(sourceId);
      const validationError = validateExplainInput(sql, connector.id);
      if (validationError) {
        success = false;
        errorMessage = validationError;
        return createToolErrorResponse(errorMessage, "INVALID_INPUT");
      }
      const explainStatement = buildExplainStatement(connector.id, sql);
      result = await connector.executeSQL(explainStatement, { readonly: true });
      const responseData = {
        rows: result.resultSets[0].rows,
        count: result.resultSets[0].rowCount,
        source_id: effectiveSourceId,
        ...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
      };
      return createToolSuccessResponse(responseData);
    } catch (error) {
      success = false;
      errorMessage = error.message;
      const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
      if (classified) return classified;
      return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
    } finally {
      trackToolRequest(
        {
          sourceId: effectiveSourceId,
          toolName: resolveTrackedToolName(sourceId, "explain_sql"),
          sql
        },
        startTime,
        extra,
        success,
        errorMessage
      );
    }
  };
}

// src/tools/health-check.ts
import { z as z4 } from "zod";
var healthCheckSchema = {};
var healthCheckInputSchema = z4.object(healthCheckSchema);
function createHealthCheckToolHandler(sourceId) {
  return async (_args, extra) => {
    const startTime = Date.now();
    const effectiveSourceId = getEffectiveSourceId(sourceId);
    let success = true;
    let errorMessage;
    try {
      await ConnectorManager.ensureConnected(sourceId);
      const connector = ConnectorManager.getCurrentConnector(sourceId);
      if (!connector.getHealthCheck) {
        success = false;
        errorMessage = `health_check is not supported for '${connector.id}' sources yet`;
        return createToolErrorResponse(errorMessage, "UNSUPPORTED");
      }
      const health = await connector.getHealthCheck();
      return createToolSuccessResponse({
        source_id: effectiveSourceId,
        ...health.connections ? {
          connections: {
            total: health.connections.total,
            active: health.connections.active,
            idle: health.connections.idle,
            idle_in_transaction: health.connections.idleInTransaction,
            ...health.connections.idleInTransactionAborted !== void 0 ? { idle_in_transaction_aborted: health.connections.idleInTransactionAborted } : {},
            max_connections: health.connections.maxConnections,
            longest_idle_in_transaction_seconds: health.connections.longestIdleInTransactionSeconds,
            longest_active_query_seconds: health.connections.longestActiveQuerySeconds
          }
        } : {},
        ...health.bufferCache ? {
          buffer_cache: {
            hit_ratio_pct: health.bufferCache.hitRatioPct,
            blocks_hit: health.bufferCache.blocksHit,
            blocks_read: health.bufferCache.blocksRead
          }
        } : {},
        ...health.notes && health.notes.length > 0 ? { notes: health.notes } : {}
      });
    } catch (error) {
      success = false;
      errorMessage = error.message;
      const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
      if (classified) return classified;
      return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
    } finally {
      trackToolRequest(
        {
          sourceId: effectiveSourceId,
          toolName: effectiveSourceId === "default" ? "health_check" : `health_check_${effectiveSourceId}`,
          sql: ""
        },
        startTime,
        extra,
        success,
        errorMessage
      );
    }
  };
}

// src/utils/tool-metadata.ts
import { z as z5 } from "zod";
function buildSourceDescriptionPrefix(description) {
  const trimmed = description?.trim() ?? "";
  if (!trimmed) return "";
  return /[.!?:]$/.test(trimmed) ? `${trimmed} ` : `${trimmed}. `;
}
function zodToParameters(schema) {
  const parameters = [];
  for (const [key, zodType] of Object.entries(schema)) {
    const description = zodType.description || "";
    const required = !(zodType instanceof z5.ZodOptional);
    let type = "string";
    if (zodType instanceof z5.ZodString) {
      type = "string";
    } else if (zodType instanceof z5.ZodNumber) {
      type = "number";
    } else if (zodType instanceof z5.ZodBoolean) {
      type = "boolean";
    } else if (zodType instanceof z5.ZodArray) {
      type = "array";
    } else if (zodType instanceof z5.ZodObject) {
      type = "object";
    }
    parameters.push({
      name: key,
      type,
      required,
      description
    });
  }
  return parameters;
}
function resolveBuiltinToolNaming(sourceId, baseName, titleLabel) {
  const sourceIds = ConnectorManager.getAvailableSourceIds();
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
  const dbType = sourceConfig.type;
  const isSingleSource = sourceIds.length === 1;
  const name = isSingleSource ? baseName : `${baseName}_${normalizeSourceId(sourceId)}`;
  const title = isSingleSource ? `${titleLabel} (${dbType})` : `${titleLabel} on ${sourceId} (${dbType})`;
  const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
  return { dbType, isSingleSource, name, title, userDescPrefix };
}
function getExecuteSqlMetadata(sourceId) {
  const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
    sourceId,
    "execute_sql",
    "Execute SQL"
  );
  const registry = getToolRegistry();
  const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
  const executeOptions = {
    readonly: toolConfig?.readonly,
    maxRows: toolConfig?.max_rows
  };
  const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
  const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows; capped results carry "truncated": true)` : "";
  const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
  const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
  const annotations = {
    title,
    readOnlyHint: isReadonly,
    destructiveHint: !isReadonly,
    // Can be destructive if not readonly
    // In readonly mode, queries are more predictable (though still not strictly idempotent due to data changes)
    // In write mode, queries are definitely not idempotent
    idempotentHint: false,
    // Database operations are always against internal/closed systems, not open-world
    openWorldHint: false
  };
  return {
    name,
    description,
    schema: executeSqlSchema,
    annotations
  };
}
function getSearchObjectsMetadata(sourceId) {
  const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
    sourceId,
    "search_objects",
    "Search Database Objects"
  );
  const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
  return { name, description, title };
}
function getExplainSqlMetadata(sourceId) {
  const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
    sourceId,
    "explain_sql",
    "Explain Query Plan"
  );
  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).`;
  const annotations = {
    title,
    readOnlyHint: true,
    destructiveHint: false,
    idempotentHint: true,
    openWorldHint: false
  };
  return {
    name,
    description,
    schema: explainSqlSchema,
    annotations
  };
}
function getHealthCheckMetadata(sourceId) {
  const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
    sourceId,
    "health_check",
    "Health Check"
  );
  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).`;
  const annotations = {
    title,
    readOnlyHint: true,
    destructiveHint: false,
    idempotentHint: false,
    // Metrics change from call to call, unlike explain_sql's plan output
    openWorldHint: false
  };
  return {
    name,
    description,
    schema: healthCheckSchema,
    annotations
  };
}
function customParamsToToolParams(params) {
  if (!params || params.length === 0) {
    return [];
  }
  return params.map((param) => ({
    name: param.name,
    type: param.type,
    required: param.required !== false && param.default === void 0,
    description: param.description
  }));
}
function buildExecuteSqlTool(sourceId, toolConfig) {
  const executeSqlMetadata = getExecuteSqlMetadata(sourceId);
  const executeSqlParameters = zodToParameters(executeSqlMetadata.schema);
  const readonly = toolConfig && "readonly" in toolConfig ? toolConfig.readonly : void 0;
  const max_rows = toolConfig && "max_rows" in toolConfig ? toolConfig.max_rows : void 0;
  return {
    name: executeSqlMetadata.name,
    description: executeSqlMetadata.description,
    parameters: executeSqlParameters,
    readonly,
    max_rows
  };
}
function buildSearchObjectsTool(sourceId) {
  const searchMetadata = getSearchObjectsMetadata(sourceId);
  return {
    name: searchMetadata.name,
    description: searchMetadata.description,
    parameters: [
      {
        name: "object_type",
        type: "string",
        required: true,
        description: "Object type to search"
      },
      {
        name: "pattern",
        type: "string",
        required: false,
        description: "LIKE pattern (% = any chars, _ = one char)"
      },
      {
        name: "schema",
        type: "string",
        required: false,
        description: "Filter to schema"
      },
      {
        name: "table",
        type: "string",
        required: false,
        description: "Filter to table (requires schema; column/index only)"
      },
      {
        name: "detail_level",
        type: "string",
        required: false,
        description: "Detail: names (minimal), summary (metadata), full (all)"
      },
      {
        name: "limit",
        type: "integer",
        required: false,
        description: "Max results"
      }
    ],
    readonly: true
    // search_objects is always readonly
  };
}
function buildExplainSqlTool(sourceId) {
  const explainMetadata = getExplainSqlMetadata(sourceId);
  return {
    name: explainMetadata.name,
    description: explainMetadata.description,
    parameters: zodToParameters(explainMetadata.schema),
    readonly: true
    // explain_sql is always readonly
  };
}
function buildCustomTool(toolConfig) {
  return {
    name: toolConfig.name,
    description: toolConfig.description,
    parameters: customParamsToToolParams(toolConfig.parameters),
    statement: toolConfig.statement,
    readonly: toolConfig.readonly,
    max_rows: toolConfig.max_rows
  };
}
function getToolsForSource(sourceId) {
  const registry = getToolRegistry();
  const enabledToolConfigs = registry.getEnabledToolConfigs(sourceId);
  return enabledToolConfigs.map((toolConfig) => {
    if (toolConfig.name === "execute_sql") {
      return buildExecuteSqlTool(sourceId, toolConfig);
    } else if (toolConfig.name === "search_objects") {
      return buildSearchObjectsTool(sourceId);
    } else if (toolConfig.name === "explain_sql") {
      return buildExplainSqlTool(sourceId);
    } else {
      return buildCustomTool(toolConfig);
    }
  });
}

// src/tools/custom-tool-handler.ts
import { z as z6 } from "zod";
function buildZodSchemaFromParameters(parameters) {
  if (!parameters || parameters.length === 0) {
    return {};
  }
  const schemaShape = {};
  for (const param of parameters) {
    let fieldSchema;
    switch (param.type) {
      case "string":
        fieldSchema = z6.string().describe(param.description);
        break;
      case "integer":
        fieldSchema = z6.number().int().describe(param.description);
        break;
      case "float":
        fieldSchema = z6.number().describe(param.description);
        break;
      case "boolean":
        fieldSchema = z6.boolean().describe(param.description);
        break;
      case "array":
        fieldSchema = z6.array(z6.unknown()).describe(param.description);
        break;
      default:
        throw new Error(`Unsupported parameter type: ${param.type}`);
    }
    if (param.allowed_values && param.allowed_values.length > 0) {
      if (param.type === "string") {
        fieldSchema = z6.enum(param.allowed_values).describe(param.description);
      } else {
        fieldSchema = fieldSchema.refine(
          (val) => param.allowed_values.includes(val),
          {
            message: `Value must be one of: ${param.allowed_values.join(", ")}`
          }
        );
      }
    }
    if (param.default !== void 0 || param.required === false) {
      fieldSchema = fieldSchema.optional();
    }
    schemaShape[param.name] = fieldSchema;
  }
  return schemaShape;
}
var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
function getCustomToolInputSchema(toolConfig) {
  let schema = customToolInputSchemas.get(toolConfig);
  if (!schema) {
    schema = z6.object(buildZodSchemaFromParameters(toolConfig.parameters));
    customToolInputSchemas.set(toolConfig, schema);
  }
  return schema;
}
function createCustomToolHandler(toolConfig) {
  const zodSchema = getCustomToolInputSchema(toolConfig);
  return async (args, extra) => {
    const startTime = Date.now();
    let success = true;
    let errorMessage;
    let paramValues = [];
    try {
      const validatedArgs = zodSchema.parse(args);
      await ConnectorManager.ensureConnected(toolConfig.source);
      const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
      const policy = policyFromReadonly(toolConfig.readonly);
      const executeOptions = {
        readonly: isReadOnlyPolicy(policy),
        maxRows: toolConfig.max_rows
      };
      if (sqlVerdict(policy, toolConfig.statement, connector.id) === "deny") {
        errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
        success = false;
        return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
      }
      paramValues = mapArgumentsToArray(
        toolConfig.parameters,
        validatedArgs
      );
      const result = await connector.executeSQL(
        toolConfig.statement,
        executeOptions,
        paramValues
      );
      const responseData = {
        statements: toStatementsPayload(result.resultSets),
        source_id: toolConfig.source
      };
      return createToolSuccessResponse(responseData);
    } catch (error) {
      success = false;
      errorMessage = error.message;
      const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
      if (classified) return classified;
      if (error instanceof z6.ZodError) {
        const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
        errorMessage = `Parameter validation failed: ${issues}`;
      } else {
        errorMessage = `${errorMessage}

SQL: ${toolConfig.statement}
Parameters: ${JSON.stringify(paramValues)}`;
      }
      return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
    } finally {
      trackToolRequest(
        {
          sourceId: toolConfig.source,
          toolName: toolConfig.name,
          sql: toolConfig.statement
        },
        startTime,
        extra,
        success,
        errorMessage
      );
    }
  };
}

// src/tools/index.ts
function registerTools(server) {
  const sourceIds = ConnectorManager.getAvailableSourceIds();
  if (sourceIds.length === 0) {
    throw new Error("No database sources configured");
  }
  const registry = getToolRegistry();
  for (const sourceId of sourceIds) {
    const enabledTools = registry.getEnabledToolConfigs(sourceId);
    for (const toolConfig of enabledTools) {
      if (toolConfig.name === BUILTIN_TOOL_EXECUTE_SQL) {
        registerExecuteSqlTool(server, sourceId);
      } else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
        registerSearchObjectsTool(server, sourceId);
      } else if (toolConfig.name === BUILTIN_TOOL_EXPLAIN_SQL) {
        registerExplainSqlTool(server, sourceId);
      } else if (toolConfig.name === BUILTIN_TOOL_HEALTH_CHECK) {
        registerHealthCheckTool(server, sourceId);
      } else {
        registerCustomTool(server, sourceId, toolConfig);
      }
    }
  }
}
function registerExecuteSqlTool(server, sourceId) {
  const metadata = getExecuteSqlMetadata(sourceId);
  server.registerTool(
    metadata.name,
    {
      description: metadata.description,
      inputSchema: executeSqlInputSchema,
      annotations: metadata.annotations
    },
    createExecuteSqlToolHandler(sourceId)
  );
}
function registerSearchObjectsTool(server, sourceId) {
  const metadata = getSearchObjectsMetadata(sourceId);
  server.registerTool(
    metadata.name,
    {
      description: metadata.description,
      inputSchema: searchDatabaseObjectsInputSchema,
      annotations: {
        title: metadata.title,
        readOnlyHint: true,
        destructiveHint: false,
        idempotentHint: true,
        openWorldHint: false
      }
    },
    createSearchDatabaseObjectsToolHandler(sourceId)
  );
}
function registerExplainSqlTool(server, sourceId) {
  const metadata = getExplainSqlMetadata(sourceId);
  server.registerTool(
    metadata.name,
    {
      description: metadata.description,
      inputSchema: explainSqlInputSchema,
      annotations: metadata.annotations
    },
    createExplainSqlToolHandler(sourceId)
  );
}
function registerHealthCheckTool(server, sourceId) {
  const metadata = getHealthCheckMetadata(sourceId);
  server.registerTool(
    metadata.name,
    {
      description: metadata.description,
      inputSchema: healthCheckInputSchema,
      annotations: metadata.annotations
    },
    createHealthCheckToolHandler(sourceId)
  );
}
function registerCustomTool(server, sourceId, toolConfig) {
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
  const dbType = sourceConfig.type;
  const isReadOnly = classifySQL(toolConfig.statement, dbType) === "read";
  server.registerTool(
    toolConfig.name,
    {
      description: toolConfig.description,
      inputSchema: getCustomToolInputSchema(toolConfig),
      annotations: {
        title: `${toolConfig.name} (${dbType})`,
        readOnlyHint: isReadOnly,
        destructiveHint: !isReadOnly,
        idempotentHint: isReadOnly,
        openWorldHint: false
      }
    },
    createCustomToolHandler(toolConfig)
  );
}

// src/api/sources.ts
function transformSourceConfig(source) {
  if (!source.type && source.dsn) {
    const inferredType = getDatabaseTypeFromDSN(source.dsn);
    if (inferredType) {
      source.type = inferredType;
    }
  }
  if (!source.type) {
    throw new Error(`Source ${source.id} is missing required type field`);
  }
  const dataSource = {
    id: source.id,
    type: source.type
  };
  if (source.description) {
    dataSource.description = source.description;
  }
  if (source.host) {
    dataSource.host = source.host;
  }
  if (source.port !== void 0) {
    dataSource.port = source.port;
  }
  if (source.database) {
    dataSource.database = source.database;
  }
  if (source.user) {
    dataSource.user = source.user;
  }
  if (source.ssh_host) {
    const sshTunnel = {
      enabled: true,
      ssh_host: source.ssh_host
    };
    if (source.ssh_port !== void 0) {
      sshTunnel.ssh_port = source.ssh_port;
    }
    if (source.ssh_user) {
      sshTunnel.ssh_user = source.ssh_user;
    }
    dataSource.ssh_tunnel = sshTunnel;
  }
  dataSource.tools = getToolsForSource(source.id);
  return dataSource;
}
function listSources(req, res) {
  try {
    const sourceConfigs = ConnectorManager.getAllSourceConfigs();
    const sources = sourceConfigs.map((config) => {
      return transformSourceConfig(config);
    });
    res.json(sources);
  } catch (error) {
    console.error("Error listing sources:", error);
    const errorResponse = {
      error: error instanceof Error ? error.message : "Internal server error"
    };
    res.status(500).json(errorResponse);
  }
}
function getSource(req, res) {
  try {
    const sourceId = req.params.sourceId;
    const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
    if (!sourceConfig) {
      const errorResponse = {
        error: "Source not found",
        source_id: sourceId
      };
      res.status(404).json(errorResponse);
      return;
    }
    const dataSource = transformSourceConfig(sourceConfig);
    res.json(dataSource);
  } catch (error) {
    console.error(`Error getting source ${req.params.sourceId}:`, error);
    const errorResponse = {
      error: error instanceof Error ? error.message : "Internal server error"
    };
    res.status(500).json(errorResponse);
  }
}

// src/api/requests.ts
function listRequests(req, res) {
  try {
    const sourceId = req.query.source_id;
    const requests = requestStore.getAll(sourceId);
    res.json({
      requests,
      total: requests.length
    });
  } catch (error) {
    console.error("Error listing requests:", error);
    res.status(500).json({
      error: error instanceof Error ? error.message : "Internal server error"
    });
  }
}

// src/utils/startup-table.ts
var BOX = {
  topLeft: "\u250C",
  topRight: "\u2510",
  bottomLeft: "\u2514",
  bottomRight: "\u2518",
  horizontal: "\u2500",
  vertical: "\u2502",
  leftT: "\u251C",
  rightT: "\u2524",
  bullet: "\u2022"
};
function parseHostAndDatabase(source) {
  if (source.dsn) {
    const parsed = parseConnectionInfoFromDSN(source.dsn);
    if (parsed) {
      if (parsed.type === "sqlite") {
        return { host: "", database: parsed.database || ":memory:" };
      }
      if (!parsed.host) {
        return { host: "", database: parsed.database || "" };
      }
      const port = parsed.port ?? getDefaultPortForType(parsed.type);
      const host2 = port ? `${parsed.host}:${port}` : parsed.host;
      return { host: host2, database: parsed.database || "" };
    }
    return { host: "unknown", database: "" };
  }
  const host = source.host ? source.port ? `${source.host}:${source.port}` : source.host : "";
  const database = source.database || "";
  return { host, database };
}
function horizontalLine(width, left, right) {
  return left + BOX.horizontal.repeat(width - 2) + right;
}
function fitString(str, width) {
  if (str.length > width) {
    return str.slice(0, width - 1) + "\u2026";
  }
  return str.padEnd(width);
}
function formatHostDatabase(host, database) {
  return host ? database ? `${host}/${database}` : host : database || "";
}
function generateStartupTable(sources) {
  if (sources.length === 0) {
    return "";
  }
  const idTypeWidth = Math.max(
    20,
    ...sources.map((s) => `${s.id} (${s.type})`.length)
  );
  const hostDbWidth = Math.max(
    24,
    ...sources.map((s) => formatHostDatabase(s.host, s.database).length)
  );
  const modeWidth = Math.max(
    10,
    ...sources.map((s) => {
      const modes = [];
      if (s.isDemo) modes.push("DEMO");
      if (s.readonly) modes.push("READ-ONLY");
      return modes.join(" ").length;
    })
  );
  const totalWidth = 2 + idTypeWidth + 3 + hostDbWidth + 3 + modeWidth + 2;
  const lines = [];
  for (let i = 0; i < sources.length; i++) {
    const source = sources[i];
    const isFirst = i === 0;
    const isLast = i === sources.length - 1;
    if (isFirst) {
      lines.push(horizontalLine(totalWidth, BOX.topLeft, BOX.topRight));
    }
    const idType = fitString(`${source.id} (${source.type})`, idTypeWidth);
    const hostDb = fitString(
      formatHostDatabase(source.host, source.database),
      hostDbWidth
    );
    const modes = [];
    if (source.isDemo) modes.push("DEMO");
    if (source.readonly) modes.push("READ-ONLY");
    const modeStr = fitString(modes.join(" "), modeWidth);
    lines.push(
      `${BOX.vertical} ${idType} ${BOX.vertical} ${hostDb} ${BOX.vertical} ${modeStr} ${BOX.vertical}`
    );
    lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
    for (const tool of source.tools) {
      const toolLine = `  ${BOX.bullet} ${tool}`;
      lines.push(
        `${BOX.vertical} ${fitString(toolLine, totalWidth - 4)} ${BOX.vertical}`
      );
    }
    if (isLast) {
      lines.push(horizontalLine(totalWidth, BOX.bottomLeft, BOX.bottomRight));
    } else {
      lines.push(horizontalLine(totalWidth, BOX.leftT, BOX.rightT));
    }
  }
  return lines.join("\n");
}
function buildSourceDisplayInfo(sourceConfigs, getToolsForSource2, isDemo) {
  return sourceConfigs.map((source) => {
    const { host, database } = parseHostAndDatabase(source);
    return {
      id: source.id,
      type: source.type || "sqlite",
      host,
      database,
      readonly: source.readonly || false,
      isDemo,
      tools: getToolsForSource2(source.id)
    };
  });
}

// src/utils/config-watcher.ts
import fs from "fs";
var DEBOUNCE_MS = 500;
function startConfigWatcher(options) {
  const { connectorManager, initialTools } = options;
  const configPath = resolveTomlConfigPath();
  if (!configPath) {
    return null;
  }
  let debounceTimer = null;
  let isReloading = false;
  let reloadPending = false;
  let lastGoodSources = connectorManager.getAllSourceConfigs();
  let lastGoodTools = initialTools;
  const scheduleReload = () => {
    if (debounceTimer) {
      clearTimeout(debounceTimer);
    }
    debounceTimer = setTimeout(reload, DEBOUNCE_MS);
  };
  const reload = async () => {
    if (isReloading) {
      reloadPending = true;
      return;
    }
    isReloading = true;
    reloadPending = false;
    try {
      console.error(`
Detected change in ${configPath}, reloading configuration...`);
      const newConfig = loadTomlConfig();
      if (!newConfig) {
        console.error("Config reload: failed to load TOML config, keeping existing connections.");
        return;
      }
      const oldSources = lastGoodSources;
      const oldTools = lastGoodTools;
      await connectorManager.disconnect();
      try {
        await connectorManager.connectWithSources(newConfig.sources);
        initializeToolRegistry({
          sources: newConfig.sources,
          tools: newConfig.tools
        });
        lastGoodSources = newConfig.sources;
        lastGoodTools = newConfig.tools;
        console.error("Configuration reloaded successfully.");
      } catch (connectError) {
        console.error("Failed to connect with new config, rolling back:", connectError);
        try {
          await connectorManager.disconnect();
        } catch {
        }
        try {
          await connectorManager.connectWithSources(oldSources);
          initializeToolRegistry({ sources: oldSources, tools: oldTools });
          console.error("Rolled back to previous configuration.");
        } catch (rollbackError) {
          console.error("Rollback also failed, server has no active connections:", rollbackError);
        }
      }
    } catch (error) {
      console.error("Config reload failed, keeping existing connections:", error);
    } finally {
      isReloading = false;
      if (reloadPending) {
        reloadPending = false;
        scheduleReload();
      }
    }
  };
  const watcher = fs.watch(configPath, (eventType) => {
    if (eventType === "change") {
      scheduleReload();
    }
  });
  watcher.unref?.();
  watcher.on("error", (err) => {
    console.error("Config file watcher error:", err);
  });
  console.error(`Watching ${configPath} for changes (hot reload enabled)`);
  return () => {
    if (debounceTimer) {
      clearTimeout(debounceTimer);
    }
    watcher.close();
  };
}

// src/utils/cross-origin.ts
import os from "node:os";
var INVALID_HOST_CHARS = /[\s/\\@?#]/;
var LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
var ALLOW_ANY_HOST = "*";
function normalizeHost(raw) {
  const trimmed = raw.trim();
  if (!trimmed) return null;
  if (trimmed === ALLOW_ANY_HOST) return ALLOW_ANY_HOST;
  if (INVALID_HOST_CHARS.test(trimmed)) return null;
  try {
    const hostname = new URL(`http://${trimmed}`).hostname.toLowerCase();
    return hostname || null;
  } catch {
    return null;
  }
}
function getSelfHosts() {
  const hosts = [];
  try {
    const hostname = os.hostname().trim();
    if (hostname) hosts.push(hostname);
  } catch {
  }
  let interfaces = {};
  try {
    interfaces = os.networkInterfaces();
  } catch {
  }
  for (const addrs of Object.values(interfaces)) {
    for (const addr of addrs ?? []) {
      if (!addr.address || addr.internal) continue;
      const isIPv6 = addr.family === "IPv6" || addr.family === 6;
      if (isIPv6) {
        const bare = addr.address.split("%")[0];
        if (bare.toLowerCase().startsWith("fe80")) continue;
        hosts.push(`[${bare}]`);
      } else {
        hosts.push(addr.address);
      }
    }
  }
  return hosts;
}
function buildAllowedHosts(configured = [], bindHost, selfHosts = []) {
  const normalizedConfigured = configured.map(normalizeHost).filter((h) => h !== null);
  if (normalizedConfigured.includes(ALLOW_ANY_HOST)) {
    return /* @__PURE__ */ new Set([ALLOW_ANY_HOST]);
  }
  const hosts = /* @__PURE__ */ new Set();
  for (const h of LOOPBACK_HOSTS) {
    const normalized = normalizeHost(h);
    if (normalized) hosts.add(normalized);
  }
  const normalizedBind = bindHost ? normalizeHost(bindHost) : null;
  const bindIsWildcard = !normalizedBind || normalizedBind === "0.0.0.0" || normalizedBind === "[::]";
  if (normalizedBind && !bindIsWildcard) {
    hosts.add(normalizedBind);
  }
  if (bindIsWildcard) {
    for (const h of selfHosts) {
      const normalized = normalizeHost(h);
      if (normalized) hosts.add(normalized);
    }
  }
  for (const h of normalizedConfigured) hosts.add(h);
  return hosts;
}
function validateOrigin(originHeader, hostHeader, allowedHosts) {
  const allowAny = allowedHosts.has(ALLOW_ANY_HOST);
  const trimmedHost = (hostHeader ?? "").trim();
  if (!trimmedHost || INVALID_HOST_CHARS.test(trimmedHost)) {
    return { ok: false, status: 400, message: "Malformed Host header" };
  }
  let hostname;
  try {
    hostname = new URL(`http://${trimmedHost}`).hostname.toLowerCase();
  } catch {
    return { ok: false, status: 400, message: "Malformed Host header" };
  }
  if (!hostname) {
    return { ok: false, status: 400, message: "Malformed Host header" };
  }
  if (!allowAny && !allowedHosts.has(hostname)) {
    return {
      ok: false,
      status: 403,
      message: `Host '${hostname}' is not allowed. Only loopback is permitted by default; set --allowed-hosts (or DBHUB_ALLOWED_HOSTS) to serve other hostnames. This protects against DNS rebinding.`
    };
  }
  if (originHeader === void 0) return { ok: true };
  const trimmedOrigin = originHeader.trim();
  if (!trimmedOrigin) {
    return { ok: false, status: 400, message: "Malformed Origin header" };
  }
  let originHostname;
  try {
    originHostname = new URL(trimmedOrigin).hostname.toLowerCase();
  } catch {
    return { ok: false, status: 400, message: "Malformed Origin header" };
  }
  if (!originHostname) {
    return { ok: false, status: 400, message: "Malformed Origin header" };
  }
  if (!allowAny && !allowedHosts.has(originHostname)) {
    return {
      ok: false,
      status: 403,
      message: `Origin '${originHostname}' is not allowed`
    };
  }
  return { ok: true };
}

// src/utils/auth-token.ts
import { timingSafeEqual } from "node:crypto";
var BEARER_PREFIX = "Bearer ";
function constantTimeEqual(a, b) {
  const bufA = Buffer.from(a);
  const bufB = Buffer.from(b);
  if (bufA.length !== bufB.length) return false;
  return timingSafeEqual(bufA, bufB);
}
function validateAuthToken(authorizationHeader, tokens) {
  if (tokens.length === 0) return { ok: true };
  if (!authorizationHeader || !authorizationHeader.startsWith(BEARER_PREFIX)) {
    return {
      ok: false,
      status: 401,
      message: "Missing or malformed Authorization header. Expected: Bearer <token>"
    };
  }
  const presented = authorizationHeader.slice(BEARER_PREFIX.length);
  const matches = tokens.some((token) => constantTimeEqual(presented, token));
  if (!matches) {
    return { ok: false, status: 401, message: "Invalid bearer token" };
  }
  return { ok: true };
}

// src/server.ts
var __filename = fileURLToPath(import.meta.url);
var __dirname = path.dirname(__filename);
var packageJsonPath = path.join(__dirname, "..", "package.json");
var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
var SERVER_NAME = "DBHub MCP Server";
var SERVER_VERSION = packageJson.version;
var TOOLS_LIST_CACHE_TTL_MS = 3e5;
var logMcpError = (scope) => (error) => console.error(`MCP ${scope} error:`, error);
function generateBanner(version, modes = []) {
  const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
  return `
 _____  ____  _   _       _     
|  __ \\|  _ \\| | | |     | |    
| |  | | |_) | |_| |_   _| |__  
| |  | |  _ <|  _  | | | | '_ \\ 
| |__| | |_) | | | | |_| | |_) |
|_____/|____/|_| |_|\\__,_|_.__/ 
                                
v${version}${modeText} - Minimal Database MCP Server
`;
}
async function main() {
  try {
    const sourceConfigsData = await resolveSourceConfigs();
    if (!sourceConfigsData) {
      const samples = ConnectorRegistry.getAllSampleDSNs();
      const sampleFormats = Object.entries(samples).map(([id, dsn]) => `  - ${id}: ${dsn}`).join("\n");
      console.error(`
ERROR: Database connection configuration is required.
Please provide configuration in one of these ways (in order of priority):

1. Use demo mode: --demo (uses in-memory SQLite with sample employee database)
2. TOML config file: --config=path/to/dbhub.toml or ./dbhub.toml
3. Command line argument: --dsn="your-connection-string"
4. Environment variable: export DSN="your-connection-string"
5. .env file: DSN=your-connection-string

Example DSN formats:
${sampleFormats}

Example TOML config (dbhub.toml):
  [[sources]]
  id = "my_db"
  dsn = "postgres://user:pass@localhost:5432/dbname"

See documentation for more details on configuring database connections.
`);
      process.exit(1);
    }
    const connectorManager = new ConnectorManager();
    const sources = sourceConfigsData.sources;
    console.error(`Configuration source: ${sourceConfigsData.source}`);
    await connectorManager.connectWithSources(sources);
    const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-C7X5G7PA.js");
    initializeToolRegistry2({
      sources: sourceConfigsData.sources,
      tools: sourceConfigsData.tools
    });
    console.error("Tool registry initialized");
    const stopConfigWatcher = startConfigWatcher({
      connectorManager,
      initialTools: sourceConfigsData.tools
    });
    const createServer = () => {
      const server = new McpServer(
        {
          name: SERVER_NAME,
          version: SERVER_VERSION
        },
        {
          // `private` keeps caching client-side (no shared/proxy caches).
          cacheHints: {
            "tools/list": { ttlMs: TOOLS_LIST_CACHE_TTL_MS, cacheScope: "private" }
          }
        }
      );
      registerTools(server);
      return server;
    };
    const transportData = resolveTransport();
    const port = transportData.type === "http" ? resolvePort().port : null;
    const host = transportData.type === "http" ? resolveHost().host : null;
    const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
    const { tokens: authTokens, source: authTokenSource } = resolveAuthTokens();
    const activeModes = [];
    const modeDescriptions = [];
    const isDemo = isDemoMode();
    if (isDemo) {
      activeModes.push("DEMO");
      modeDescriptions.push("using sample employee database");
    }
    if (activeModes.length > 0) {
      console.error(`Running in ${activeModes.join(" and ")} mode - ${modeDescriptions.join(", ")}`);
    }
    console.error(generateBanner(SERVER_VERSION, activeModes));
    const sourceDisplayInfos = buildSourceDisplayInfo(
      sources,
      (sourceId) => getToolsForSource(sourceId).map((t) => t.readonly ? `\u{1F512} ${t.name}` : t.name),
      isDemo
    );
    console.error(generateStartupTable(sourceDisplayInfos));
    process.on("exit", () => {
      stopConfigWatcher?.();
    });
    if (transportData.type === "http") {
      const app = express();
      app.use(express.json());
      app.use((req, res, next) => {
        const origin = req.headers.origin;
        const result = validateOrigin(origin, req.headers.host, allowedHosts);
        if (!result.ok) {
          return res.status(result.status).json({
            error: result.status === 400 ? "Bad Request" : "Forbidden",
            message: result.message
          });
        }
        res.header("Access-Control-Allow-Origin", origin || "http://localhost");
        res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
        res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
        res.header("Access-Control-Allow-Credentials", "true");
        if (req.method === "OPTIONS") {
          return res.sendStatus(200);
        }
        next();
      });
      app.get("/healthz", (req, res) => {
        res.status(200).send("OK");
      });
      app.use((req, res, next) => {
        const result = validateAuthToken(req.headers.authorization, authTokens);
        if (!result.ok) {
          res.header("WWW-Authenticate", "Bearer");
          return res.status(result.status).json({ error: "Unauthorized", message: result.message });
        }
        next();
      });
      const frontendPath = path.join(__dirname, "public");
      app.use(express.static(frontendPath));
      app.get("/api/sources", listSources);
      app.get("/api/sources/:sourceId", getSource);
      app.get("/api/requests", listRequests);
      const mcpHandler = createMcpHandler(createServer, { onerror: logMcpError("handler") });
      const mcpNodeHandler = toNodeHandler(mcpHandler, { onerror: logMcpError("adapter") });
      app.all("/mcp", (req, res) => {
        void mcpNodeHandler(req, res, req.body);
      });
      if (process.env.NODE_ENV !== "development") {
        app.get("*", (req, res) => {
          res.sendFile(path.join(frontendPath, "index.html"));
        });
      }
      const httpServer = http.createServer(app);
      httpServer.on("error", (err) => {
        const displayHost = host.includes(":") ? `[${host}]` : host;
        console.error(`Failed to bind HTTP server to ${displayHost}:${port}: ${err.message}`);
        process.exit(1);
      });
      httpServer.listen(port, host, () => {
        const address = httpServer.address();
        const boundHost = typeof address === "object" && address ? address.address : host;
        const boundPort = typeof address === "object" && address ? address.port : port;
        const displayHost = boundHost.includes(":") ? `[${boundHost}]` : boundHost;
        const userHost = boundHost === "0.0.0.0" || boundHost === "::" ? "localhost" : displayHost;
        console.error(`HTTP server listening on ${displayHost}:${boundPort}`);
        if (allowedHosts.has(ALLOW_ANY_HOST)) {
          console.error("Allowed hosts: * (DNS-rebinding protection DISABLED \u2014 ensure DBHub is fronted by your own auth/proxy)");
        } else {
          console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
        }
        if (authTokens.length > 0) {
          console.error(`Auth: bearer token required (${authTokens.length} token(s) configured via ${authTokenSource})`);
        } else {
          console.error("Auth: disabled (set --auth-token or DBHUB_AUTH_TOKEN to require a bearer token)");
        }
        if (process.env.NODE_ENV === "development") {
          console.error("Development mode detected!");
          console.error("   Workbench dev server (with HMR): http://localhost:5173");
          console.error(`   Backend API: http://localhost:${boundPort}`);
          console.error("");
        } else {
          console.error(`Workbench at http://${userHost}:${boundPort}/`);
        }
        console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
      });
    } else {
      const stdioHandle = serveStdio(createServer, { onerror: logMcpError("stdio") });
      console.error("MCP server running on stdio");
      let isShuttingDown = false;
      const shutdown = async () => {
        if (isShuttingDown) return;
        isShuttingDown = true;
        console.error("Shutting down...");
        await stdioHandle.close();
        await connectorManager.disconnect();
        process.exit(0);
      };
      process.on("SIGINT", shutdown);
      process.on("SIGTERM", shutdown);
      process.stdin.on("end", shutdown);
    }
  } catch (err) {
    console.error("Fatal error:", err);
    process.exit(1);
  }
}

// src/index.ts
var connectorModules = [
  { load: () => import("./postgres-DC5BDEMU.js"), name: "PostgreSQL", driver: "pg" },
  { load: () => import("./sqlserver-AJUK6L55.js"), name: "SQL Server", driver: "mssql" },
  { load: () => import("./sqlite-AO2BKIC6.js"), name: "SQLite", driver: "node:sqlite" },
  { load: () => import("./mysql-XXIHWQGR.js"), name: "MySQL", driver: "mysql2" },
  { load: () => import("./mariadb-L7Z55ROY.js"), name: "MariaDB", driver: "mariadb" },
  { load: () => import("./oracle-OUKBU52X.js"), name: "Oracle", driver: "oracledb" }
];
loadConnectors(connectorModules).then(() => main()).catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});