@bytebase/dbhub
Version:
Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite
2,082 lines • 71.1 kB
JavaScript
#!/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);
});