Skip to main content
Glama
config-manager.ts5.47 kB
import type { MasterConfig, RoutingConfig, ServerConfig } from '../types/config.js' import { ConfigLoader } from '../config/config-loader.js' import { Logger } from '../utils/logger.js' import { EnvironmentManager } from '../config/environment-manager.js' import { SecretManager } from '../config/secret-manager.js' export interface ConfigManagerOptions { // If provided, watch the file for changes (Node only) watch?: boolean } type Listener = (config: MasterConfig) => void export class ConfigManager { private config: MasterConfig | null = null private readonly listeners: Set<Listener> = new Set() private stopWatcher?: () => void private readonly secrets = new SecretManager() private watchPaths: string[] = [] constructor(private readonly options?: ConfigManagerOptions) {} async load(): Promise<MasterConfig> { const explicit = EnvironmentManager.getExplicitConfigPath() let loaded: MasterConfig try { loaded = await ConfigLoader.load({ path: explicit }) } catch (err) { Logger.warn('Primary config load failed; attempting env-only load', String(err)) loaded = await ConfigLoader.loadFromEnv() } const normalized = this.applyDefaults(loaded) this.config = normalized const redacted = this.secrets.redact(normalized) Logger.info('Configuration loaded', { servers: normalized.servers.length, hosting: normalized.hosting.platform, redacted, }) if (this.options?.watch) this.prepareWatcher(explicit) return normalized } getConfig(): MasterConfig { if (!this.config) throw new Error('Config not loaded') return this.config } getRouting(): RoutingConfig | undefined { return this.config?.routing } onChange(listener: Listener): () => void { this.listeners.add(listener) return () => this.listeners.delete(listener) } async reload(): Promise<void> { await this.load() if (this.config) this.emit(this.config) } stop(): void { try { this.stopWatcher?.() } catch { // ignore } } private emit(config: MasterConfig): void { for (const l of this.listeners) { try { l(config) } catch (err) { Logger.warn('Config listener threw', err) } } } private applyDefaults(cfg: MasterConfig): MasterConfig { // Shallow copy to avoid mutation const copy: MasterConfig = { ...cfg, hosting: { platform: cfg.hosting.platform ?? 'node', port: cfg.hosting.port ?? 3000, base_url: cfg.hosting.base_url, }, routing: cfg.routing ? { ...cfg.routing } : {}, master_oauth: { ...cfg.master_oauth }, servers: cfg.servers.map((s) => this.normalizeServer(s)), } return copy } private normalizeServer(s: ServerConfig): ServerConfig { const port = s.config?.port const normalized: ServerConfig = { ...s, config: { environment: s.config?.environment ?? {}, args: s.config?.args ?? [], ...(port ? { port } : {}), }, } return normalized } private prepareWatcher(explicitPath?: string): void { const isNode = Boolean((globalThis as any)?.process?.versions?.node) if (!isNode) return const { base, env } = EnvironmentManager.getConfigPaths('config') this.watchPaths = [] if (explicitPath) this.watchPaths.push(explicitPath) if (base) this.watchPaths.push(base) if (env) this.watchPaths.push(env) this.startWatcher() } private startWatcher(): void { const isNode = Boolean((globalThis as any)?.process?.versions?.node) if (!isNode || this.watchPaths.length === 0) return import('node:fs').then((fs) => { const watchers: any[] = [] const onChange = async () => { try { Logger.info('Config change detected; validating and reloading...') const prev = this.config const newCfg = await ConfigLoader.load({ path: EnvironmentManager.getExplicitConfigPath() }) const applied = this.applyDefaults(newCfg) if (prev) this.auditDiff(prev, applied) this.config = applied this.emit(this.config) } catch (err) { Logger.warn('Hot-reload failed to apply new config', String(err)) } } for (const p of this.watchPaths) { try { watchers.push((fs as any).watch(p, { persistent: false }, onChange)) } catch (err) { Logger.warn(`Failed to watch ${p}`, String(err)) } } this.stopWatcher = () => { for (const w of watchers) { try { w?.close?.() } catch { // ignore } } } }).catch((err) => Logger.warn('Failed to start config file watcher', String(err))) } private auditDiff(oldCfg: MasterConfig, newCfg: MasterConfig): void { const diff: Record<string, { from: unknown; to: unknown }> = {} const keys = new Set([...Object.keys(oldCfg), ...Object.keys(newCfg)]) for (const k of keys) { const a: any = (oldCfg as any)[k] const b: any = (newCfg as any)[k] if (JSON.stringify(a) !== JSON.stringify(b)) diff[k] = { from: a, to: b } } const redacted = this.secrets.redact(diff) // Highlight non-hot-reloadable settings if (oldCfg.hosting?.port !== newCfg.hosting?.port) { Logger.warn('Hosting port changed; restart required to apply') } Logger.info('Config change audit', redacted) } }

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/Jakedismo/master-mcp-server'

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