Skip to main content
Glama
by microsoft
config.ts10.5 kB
import dotenv from "dotenv" import { homedir } from "os" import { YAMLTryParse } from "./yaml" import { JSON5TryParse } from "./json5" import { DOT_ENV_FILENAME, DOT_ENV_GENAISCRIPT_FILENAME, MODEL_PROVIDERS, TOOL_ID, } from "./constants" import { join, resolve } from "path" import { validateJSONWithSchema } from "./schema" import { HostConfiguration } from "./hostconfiguration" import { structuralMerge } from "./merge" import { LanguageModelConfiguration, ResolvedLanguageModelConfiguration, } from "./server/messages" import { resolveLanguageModel } from "./lm" import { arrayify, deleteEmptyValues } from "./cleaners" import { errorMessage } from "./error" import schema from "../../../docs/public/schemas/config.json" import defaultConfig from "./config.json" import { CancellationOptions } from "./cancellation" import { host } from "./host" import { uniq } from "es-toolkit" import { expandHomeDir, tryReadText, tryStat } from "./fs" import { parseDefaultsFromEnv } from "./env" import debug from "debug" const dbg = debug("genaiscript:config") async function resolveGlobalConfiguration( dotEnvPaths?: string[] ): Promise<HostConfiguration> { const dirs = [homedir(), "."] const exts = ["yml", "yaml", "json"] dbg("starting to resolve global configuration") // import and merge global local files let config: HostConfiguration = structuredClone(defaultConfig) delete (config as any)["$schema"] dbg("initialized config from defaultConfig") for (const dir of dirs) { for (const ext of exts) { const filename = resolve(dir, `${TOOL_ID}.config.${ext}`) dbg(`checking file: ${filename}`) const stat = await tryStat(filename) if (!stat) continue if (!stat.isFile()) { dbg(`skipping ${filename}, not a file`) throw new Error(`config: ${filename} is a not a file`) } const fileContent = await tryReadText(filename) if (!fileContent) { dbg(`skipping ${filename}, no content`) continue } dbg(`loading ${filename}`) const parsed: HostConfiguration = ext === "yml" || ext === "yaml" ? YAMLTryParse(fileContent) : JSON5TryParse(fileContent) if (!parsed) { dbg(`failed to parse ${filename}`) throw new Error(`config: failed to parse ${filename}`) } dbg("validating config schema") const validation = validateJSONWithSchema( parsed, schema as JSONSchema ) if (validation.schemaError) { dbg( `validation error for ${filename}: ${validation.schemaError}` ) throw new Error(`config: ` + validation.schemaError) } dbg("merging parsed configuration", parsed) config = deleteEmptyValues({ include: structuralMerge( config?.include || [], parsed?.include || [] ), envFile: [ ...arrayify(parsed?.envFile), ...arrayify(config?.envFile), ], modelAliases: structuralMerge( config?.modelAliases || {}, parsed?.modelAliases || {} ), modelEncodings: structuralMerge( config?.modelEncodings || {}, parsed?.modelEncodings || {} ), secretScanners: structuralMerge( config?.secretPatterns || {}, parsed?.secretPatterns || {} ), }) } } if (process.env.GENAISCRIPT_ENV_FILE) { dbg( `adding env file from environment variable: '${process.env.GENAISCRIPT_ENV_FILE}'` ) config.envFile = [ ...(config.envFile || []), process.env.GENAISCRIPT_ENV_FILE, ] } if (dotEnvPaths?.length) { dbg(`adding env files from CLI: '${dotEnvPaths.join(", ")}'`) config.envFile = [...(config.envFile || []), ...dotEnvPaths] } if (!config.envFile?.length) { dbg("no env files found, using defaults") config.envFile = [ join(homedir(), DOT_ENV_GENAISCRIPT_FILENAME), DOT_ENV_GENAISCRIPT_FILENAME, DOT_ENV_FILENAME, ] } dbg("resolving env file paths") config.envFile = uniq( arrayify(config.envFile).map((f) => expandHomeDir(resolve(f))) ) dbg(`resolved env files: ${config.envFile.join(", ")}`) return config } /** * Reads and resolves the configuration for the host environment. * * @param dotEnvPaths - Optional array of .env file paths to consider. If provided, these paths will be prioritized. * * Steps: * - Calls `resolveGlobalConfiguration` to load base configurations from default paths and files. * - Processes specified `.env` files to load environment variables. * - Validates the existence and file type of each `.env` file. * - Loads and overrides environment variables using `dotenv`. * - Parses additional defaults from the current `process.env`. * - Ensures unique resolution of `.env` file paths. * * @returns The resolved host configuration including merged and validated settings. * * @throws An error if any provided `.env` file is invalid, unreadable, or not a file. */ export async function readConfig( dotEnvPaths?: string[] ): Promise<HostConfiguration> { dbg(`reading configuration`) const config = await resolveGlobalConfiguration(dotEnvPaths) const { envFile } = config for (const dotEnv of arrayify(envFile)) { dbg(`.env: ${dotEnv}`) const stat = await tryStat(dotEnv) if (!stat) { dbg(`ignored ${dotEnv}, not found`) } else { if (!stat.isFile()) { throw new Error(`.env: ${dotEnv} is not a file`) } dbg(`loading ${dotEnv}`) const res = dotenv.config({ path: dotEnv, debug: /dotenv/.test(process.env.DEBUG), override: true, }) if (res.error) { throw res.error } } } await parseDefaultsFromEnv(process.env) return config } /** * Resolves and outputs environment information for language model providers. * @param provider - Filters by specific provider. If not provided, resolves all providers. * @param options - Configuration options: * - token - Include tokens in the output. If false, tokens are masked. * - error - Include errors in the output. * - models - List models for each provider if supported. * - hide - Exclude hidden providers from the output. * - cancellation options - Additional cancellation options. * @returns Sorted list of resolved language model configurations, including errors if applicable. * @throws An error if there is an issue retrieving or processing configurations for a provider. */ export async function resolveLanguageModelConfigurations( provider: string, options?: { token?: boolean error?: boolean models?: boolean hide?: boolean } & CancellationOptions ): Promise<ResolvedLanguageModelConfiguration[]> { const { token, error, models, hide } = options || {} const res: ResolvedLanguageModelConfiguration[] = [] dbg("starting to resolve language model configurations") for (const modelProvider of MODEL_PROVIDERS.filter( (mp) => (!provider || mp.id === provider) && (!hide || !mp.hidden) )) { dbg(`processing model provider: ${modelProvider.id}, token: ${token}`) try { const conn: LanguageModelConfiguration & { models?: LanguageModelInfo[] } = await host.getLanguageModelConfiguration( modelProvider.id + ":*", options ) if (conn) { dbg( `retrieved connection configuration for provider: ${modelProvider.id}` ) let listError = "" if (models && token) { dbg(`listing models for provider: ${modelProvider.id}`) const lm = await resolveLanguageModel(modelProvider.id) if (lm.listModels) { const models = await lm.listModels(conn, options) if (models.ok) { dbg( `successfully listed models for provider: ${modelProvider.id}` ) conn.models = models.models } else { listError = errorMessage(models.error) || "failed to list models" dbg( `error listing models for provider ${modelProvider.id}: ${listError}` ) } } } if (!token && conn.token) conn.token = "***" if (!listError || error || provider) { dbg( `adding resolved configuration for provider: ${modelProvider.id}` ) res.push( deleteEmptyValues({ provider: conn.provider, source: conn.source, base: conn.base, type: conn.type, models: conn.models, error: listError, }) ) } } } catch (e) { dbg( `error resolving configuration for provider ${modelProvider.id}: ${e}` ) if (error || provider) res.push({ provider: modelProvider.id, error: errorMessage(e), }) } } dbg("returning sorted resolved configurations") return res.sort((l, r) => l.provider.localeCompare(r.provider)) }

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/microsoft/genaiscript'

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