Skip to main content
Glama
toml-loader.ts9.77 kB
import fs from "fs"; import path from "path"; import { homedir } from "os"; import toml from "@iarna/toml"; import type { SourceConfig, TomlConfig } from "../types/config.js"; import { parseCommandLineArgs } from "./env.js"; import { parseConnectionInfoFromDSN, getDefaultPortForType } from "../utils/dsn-obfuscate.js"; /** * Load and parse TOML configuration file * Returns the parsed sources array and the source of the config file */ export function loadTomlConfig(): { sources: SourceConfig[]; source: string } | null { const configPath = resolveTomlConfigPath(); if (!configPath) { return null; } try { const fileContent = fs.readFileSync(configPath, "utf-8"); const parsedToml = toml.parse(fileContent) as unknown as TomlConfig; // Validate and process the configuration validateTomlConfig(parsedToml, configPath); const sources = processSourceConfigs(parsedToml.sources, configPath); return { sources, source: path.basename(configPath), }; } catch (error) { if (error instanceof Error) { throw new Error( `Failed to load TOML configuration from ${configPath}: ${error.message}` ); } throw error; } } /** * Resolve the path to the TOML configuration file * Priority: --config flag > ./dbhub.toml */ function resolveTomlConfigPath(): string | null { const args = parseCommandLineArgs(); // 1. Check for --config flag (highest priority) if (args.config) { const configPath = expandHomeDir(args.config); if (!fs.existsSync(configPath)) { throw new Error( `Configuration file specified by --config flag not found: ${configPath}` ); } return configPath; } // 2. Check for dbhub.toml in current directory const defaultConfigPath = path.join(process.cwd(), "dbhub.toml"); if (fs.existsSync(defaultConfigPath)) { return defaultConfigPath; } return null; } /** * Validate the structure of the parsed TOML configuration */ function validateTomlConfig(config: TomlConfig, configPath: string): void { // Check if sources array exists if (!config.sources) { throw new Error( `Configuration file ${configPath} must contain a [[sources]] array. ` + `Example:\n\n[[sources]]\nid = "my_db"\ndsn = "postgres://..."` ); } // Check if sources is an array if (!Array.isArray(config.sources)) { throw new Error( `Configuration file ${configPath}: 'sources' must be an array. ` + `Use [[sources]] syntax for array of tables in TOML.` ); } // Check if sources array is not empty if (config.sources.length === 0) { throw new Error( `Configuration file ${configPath}: sources array cannot be empty. ` + `Please define at least one source with [[sources]].` ); } // Check for duplicate IDs const ids = new Set<string>(); const duplicates: string[] = []; for (const source of config.sources) { if (!source.id) { throw new Error( `Configuration file ${configPath}: each source must have an 'id' field. ` + `Example: [[sources]]\nid = "my_db"` ); } if (ids.has(source.id)) { duplicates.push(source.id); } else { ids.add(source.id); } } if (duplicates.length > 0) { throw new Error( `Configuration file ${configPath}: duplicate source IDs found: ${duplicates.join(", ")}. ` + `Each source must have a unique 'id' field.` ); } // Validate each source has either DSN or sufficient connection parameters for (const source of config.sources) { validateSourceConfig(source, configPath); } } /** * Validate a single source configuration */ function validateSourceConfig(source: SourceConfig, configPath: string): void { const hasConnectionParams = source.type && (source.type === "sqlite" ? source.database : source.host); if (!source.dsn && !hasConnectionParams) { throw new Error( `Configuration file ${configPath}: source '${source.id}' must have either:\n` + ` - 'dsn' field (e.g., dsn = "postgres://user:pass@host:5432/dbname")\n` + ` - OR connection parameters (type, host, database, user, password)\n` + ` - For SQLite: type = "sqlite" and database path` ); } // Validate type if provided if (source.type) { const validTypes = ["postgres", "mysql", "mariadb", "sqlserver", "sqlite"]; if (!validTypes.includes(source.type)) { throw new Error( `Configuration file ${configPath}: source '${source.id}' has invalid type '${source.type}'. ` + `Valid types: ${validTypes.join(", ")}` ); } } // Validate max_rows if provided if (source.max_rows !== undefined) { if (typeof source.max_rows !== "number" || source.max_rows <= 0) { throw new Error( `Configuration file ${configPath}: source '${source.id}' has invalid max_rows. ` + `Must be a positive integer.` ); } } // Validate connection_timeout if provided if (source.connection_timeout !== undefined) { if (typeof source.connection_timeout !== "number" || source.connection_timeout <= 0) { throw new Error( `Configuration file ${configPath}: source '${source.id}' has invalid connection_timeout. ` + `Must be a positive number (in seconds).` ); } } // Validate request_timeout if provided if (source.request_timeout !== undefined) { if (typeof source.request_timeout !== "number" || source.request_timeout <= 0) { throw new Error( `Configuration file ${configPath}: source '${source.id}' has invalid request_timeout. ` + `Must be a positive number (in seconds).` ); } } // Validate SSH port if provided if (source.ssh_port !== undefined) { if ( typeof source.ssh_port !== "number" || source.ssh_port <= 0 || source.ssh_port > 65535 ) { throw new Error( `Configuration file ${configPath}: source '${source.id}' has invalid ssh_port. ` + `Must be between 1 and 65535.` ); } } } /** * Process source configurations (expand paths, populate fields from DSN) */ function processSourceConfigs( sources: SourceConfig[], configPath: string ): SourceConfig[] { return sources.map((source) => { const processed = { ...source }; // Expand ~ in SSH key path if (processed.ssh_key) { processed.ssh_key = expandHomeDir(processed.ssh_key); } // Expand ~ in SQLite database path (if relative) if (processed.type === "sqlite" && processed.database) { processed.database = expandHomeDir(processed.database); } // Expand ~ in DSN for SQLite if (processed.dsn && processed.dsn.startsWith("sqlite:///~")) { processed.dsn = `sqlite:///${expandHomeDir(processed.dsn.substring(11))}`; } // Parse DSN to populate connection info fields (if not already set) // This ensures API responses include host/port/database/user even when DSN is used if (processed.dsn) { const connectionInfo = parseConnectionInfoFromDSN(processed.dsn); if (connectionInfo) { // Only set fields that aren't already explicitly configured if (!processed.type && connectionInfo.type) { processed.type = connectionInfo.type; } if (!processed.host && connectionInfo.host) { processed.host = connectionInfo.host; } if (processed.port === undefined && connectionInfo.port !== undefined) { processed.port = connectionInfo.port; } if (!processed.database && connectionInfo.database) { processed.database = connectionInfo.database; } if (!processed.user && connectionInfo.user) { processed.user = connectionInfo.user; } } } return processed; }); } /** * Expand ~ to home directory in paths */ function expandHomeDir(filePath: string): string { if (filePath.startsWith("~/")) { return path.join(homedir(), filePath.substring(2)); } return filePath; } /** * Build DSN from source connection parameters * Similar to buildDSNFromEnvParams in env.ts but for TOML sources */ export function buildDSNFromSource(source: SourceConfig): string { // If DSN is already provided, use it if (source.dsn) { return source.dsn; } // Validate required fields if (!source.type) { throw new Error( `Source '${source.id}': 'type' field is required when 'dsn' is not provided` ); } // Handle SQLite if (source.type === "sqlite") { if (!source.database) { throw new Error( `Source '${source.id}': 'database' field is required for SQLite` ); } return `sqlite:///${source.database}`; } // For other databases, require host, user, password, database if (!source.host || !source.user || !source.password || !source.database) { throw new Error( `Source '${source.id}': missing required connection parameters. ` + `Required: type, host, user, password, database` ); } // Determine default port if not specified const port = source.port || getDefaultPortForType(source.type); if (!port) { throw new Error(`Source '${source.id}': unable to determine port`); } // Encode credentials const encodedUser = encodeURIComponent(source.user); const encodedPassword = encodeURIComponent(source.password); const encodedDatabase = encodeURIComponent(source.database); // Build base DSN let dsn = `${source.type}://${encodedUser}:${encodedPassword}@${source.host}:${port}/${encodedDatabase}`; // Add SQL Server specific query parameters if (source.type === "sqlserver" && source.instanceName) { dsn += `?instanceName=${encodeURIComponent(source.instanceName)}`; } return dsn; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bytebase/dbhub'

If you have feedback or need assistance with the MCP directory API, please join our Discord server