/**
* Configuration Manager Module
*
* Author: Yobie Benjamin
* Version: 0.2
* Date: July 28, 2025
*
* Manages all configuration for the Llama Maverick Hub.
* Handles environment variables, config files, and service definitions.
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { z } from 'zod';
import winston from 'winston';
import { MCPServiceConfig } from '../clients/mcp-client-manager.js';
const logger = winston.createLogger({
level: 'debug',
format: winston.format.simple()
});
/**
* Hub configuration schema
* Validates all configuration options
*/
const HubConfigSchema = z.object({
hub: z.object({
name: z.string().default('llama-maverick-hub'),
version: z.string().default('0.2.0'),
port: z.number().default(8080),
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info')
}),
llama: z.object({
model: z.string().default('llama3.2'),
baseUrl: z.string().default('http://localhost:11434'),
contextWindow: z.number().default(8192),
defaultTemperature: z.number().default(0.7)
}),
services: z.array(z.object({
id: z.string(),
name: z.string(),
description: z.string(),
transport: z.enum(['stdio', 'websocket', 'http']),
endpoint: z.string(),
enabled: z.boolean().default(true),
command: z.string().optional(),
args: z.array(z.string()).optional(),
url: z.string().optional(),
headers: z.record(z.string()).optional(),
reconnectPolicy: z.object({
maxRetries: z.number().default(5),
retryDelayMs: z.number().default(5000),
backoffMultiplier: z.number().default(2)
}).optional()
})).default([]),
orchestration: z.object({
maxConcurrentOperations: z.number().default(10),
defaultTimeout: z.number().default(30000),
retryPolicy: z.object({
maxRetries: z.number().default(3),
retryDelayMs: z.number().default(1000)
})
}),
security: z.object({
enableAuth: z.boolean().default(false),
apiKeys: z.array(z.string()).optional(),
allowedOrigins: z.array(z.string()).default(['*']),
rateLimiting: z.object({
enabled: z.boolean().default(true),
requestsPerMinute: z.number().default(100)
})
})
});
type HubConfig = z.infer<typeof HubConfigSchema>;
/**
* Manages all configuration for the hub
* Loads from files, environment, and provides defaults
*/
export class ConfigManager {
private config: HubConfig | null = null;
private configPath: string;
private envPrefix = 'LLAMA_HUB_';
constructor(configPath?: string) {
/**
* Set configuration file path
* Default to config.json in current directory
*/
this.configPath = configPath || path.join(process.cwd(), 'config.json');
}
/**
* Load configuration from all sources
* Merges file config with environment variables
*/
async loadConfig(): Promise<void> {
logger.info('Loading configuration...');
try {
/**
* Start with default configuration
* Provides sensible defaults for all options
*/
let config = this.getDefaultConfig();
/**
* Load from configuration file if exists
* File config overrides defaults
*/
const fileConfig = await this.loadFileConfig();
if (fileConfig) {
config = this.mergeConfigs(config, fileConfig);
}
/**
* Load from environment variables
* Environment overrides file config
*/
const envConfig = this.loadEnvConfig();
config = this.mergeConfigs(config, envConfig);
/**
* Load service definitions
* Can come from separate file or inline
*/
const services = await this.loadServiceDefinitions();
if (services.length > 0) {
config.services = services;
}
/**
* Validate final configuration
* Ensure all required fields are present
*/
this.config = HubConfigSchema.parse(config);
logger.info('Configuration loaded successfully', {
hubName: this.config.hub.name,
servicesCount: this.config.services.length
});
} catch (error) {
logger.error('Failed to load configuration:', error);
throw error;
}
}
/**
* Get default configuration
* Provides base configuration with example services
*/
private getDefaultConfig(): HubConfig {
return {
hub: {
name: 'llama-maverick-hub',
version: '0.2.0',
port: 8080,
logLevel: 'info'
},
llama: {
model: 'llama3.2',
baseUrl: 'http://localhost:11434',
contextWindow: 8192,
defaultTemperature: 0.7
},
services: [
/**
* Example Stripe MCP service configuration
* Connects to Stripe's MCP server for payment operations
*/
{
id: 'stripe',
name: 'Stripe MCP',
description: 'Stripe payment processing via MCP',
transport: 'stdio' as const,
endpoint: 'stripe-mcp',
enabled: true,
command: 'npx',
args: ['-y', '@stripe/mcp-server'],
reconnectPolicy: {
maxRetries: 5,
retryDelayMs: 5000,
backoffMultiplier: 2
}
},
/**
* Example GitHub MCP service configuration
* Connects to GitHub for repository operations
*/
{
id: 'github',
name: 'GitHub MCP',
description: 'GitHub repository management via MCP',
transport: 'stdio' as const,
endpoint: 'github-mcp',
enabled: false, // Disabled by default
command: 'github-mcp-server',
args: []
},
/**
* Example database MCP service configuration
* Connects to a database service via WebSocket
*/
{
id: 'database',
name: 'Database MCP',
description: 'Database operations via MCP',
transport: 'websocket' as const,
endpoint: 'ws://localhost:8081/mcp',
enabled: false,
url: 'ws://localhost:8081/mcp'
}
],
orchestration: {
maxConcurrentOperations: 10,
defaultTimeout: 30000,
retryPolicy: {
maxRetries: 3,
retryDelayMs: 1000
}
},
security: {
enableAuth: false,
allowedOrigins: ['*'],
rateLimiting: {
enabled: true,
requestsPerMinute: 100
}
}
};
}
/**
* Load configuration from file
* Reads JSON configuration file
*/
private async loadFileConfig(): Promise<Partial<HubConfig> | null> {
try {
const configData = await fs.readFile(this.configPath, 'utf-8');
const config = JSON.parse(configData);
logger.debug('Loaded configuration from file', { path: this.configPath });
return config;
} catch (error: any) {
if (error.code === 'ENOENT') {
logger.debug('No configuration file found, using defaults');
return null;
}
logger.error('Failed to load configuration file:', error);
throw error;
}
}
/**
* Load configuration from environment variables
* Maps environment variables to config structure
*/
private loadEnvConfig(): Partial<HubConfig> {
const config: any = {};
/**
* Map environment variables to configuration
* Uses LLAMA_HUB_ prefix for all variables
*/
// Hub configuration
if (process.env[`${this.envPrefix}NAME`]) {
config.hub = config.hub || {};
config.hub.name = process.env[`${this.envPrefix}NAME`];
}
if (process.env[`${this.envPrefix}PORT`]) {
config.hub = config.hub || {};
config.hub.port = parseInt(process.env[`${this.envPrefix}PORT`]);
}
if (process.env[`${this.envPrefix}LOG_LEVEL`]) {
config.hub = config.hub || {};
config.hub.logLevel = process.env[`${this.envPrefix}LOG_LEVEL`];
}
// Llama configuration
if (process.env[`${this.envPrefix}LLAMA_MODEL`]) {
config.llama = config.llama || {};
config.llama.model = process.env[`${this.envPrefix}LLAMA_MODEL`];
}
if (process.env[`${this.envPrefix}LLAMA_BASE_URL`]) {
config.llama = config.llama || {};
config.llama.baseUrl = process.env[`${this.envPrefix}LLAMA_BASE_URL`];
}
// Security configuration
if (process.env[`${this.envPrefix}ENABLE_AUTH`]) {
config.security = config.security || {};
config.security.enableAuth = process.env[`${this.envPrefix}ENABLE_AUTH`] === 'true';
}
if (process.env[`${this.envPrefix}API_KEYS`]) {
config.security = config.security || {};
config.security.apiKeys = process.env[`${this.envPrefix}API_KEYS`].split(',');
}
return config;
}
/**
* Load service definitions from file or environment
* Supports dynamic service configuration
*/
private async loadServiceDefinitions(): Promise<MCPServiceConfig[]> {
const services: MCPServiceConfig[] = [];
/**
* Check for services definition file
* Allows external service configuration
*/
const servicesPath = process.env[`${this.envPrefix}SERVICES_FILE`] ||
path.join(process.cwd(), 'services.json');
try {
const servicesData = await fs.readFile(servicesPath, 'utf-8');
const loadedServices = JSON.parse(servicesData);
if (Array.isArray(loadedServices)) {
services.push(...loadedServices);
logger.debug(`Loaded ${loadedServices.length} services from file`);
}
} catch (error: any) {
if (error.code !== 'ENOENT') {
logger.warn('Failed to load services file:', error);
}
}
/**
* Check for individual service environment variables
* Supports defining services via environment
*/
// Example: LLAMA_HUB_SERVICE_STRIPE_ENABLED=true
const envServices = this.parseServiceEnvVars();
services.push(...envServices);
return services;
}
/**
* Parse service definitions from environment variables
* Supports pattern: LLAMA_HUB_SERVICE_<ID>_<PROPERTY>
*/
private parseServiceEnvVars(): MCPServiceConfig[] {
const services: Map<string, Partial<MCPServiceConfig>> = new Map();
const servicePrefix = `${this.envPrefix}SERVICE_`;
for (const [key, value] of Object.entries(process.env)) {
if (!key.startsWith(servicePrefix)) continue;
// Parse service ID and property
const parts = key.slice(servicePrefix.length).split('_');
if (parts.length < 2) continue;
const serviceId = parts[0].toLowerCase();
const property = parts.slice(1).join('_').toLowerCase();
// Initialize service if needed
if (!services.has(serviceId)) {
services.set(serviceId, { id: serviceId });
}
const service = services.get(serviceId)!;
// Map property to service configuration
switch (property) {
case 'name':
service.name = value;
break;
case 'description':
service.description = value;
break;
case 'transport':
service.transport = value as any;
break;
case 'endpoint':
service.endpoint = value;
break;
case 'enabled':
service.enabled = value === 'true';
break;
case 'command':
service.command = value;
break;
case 'url':
service.url = value;
break;
}
}
return Array.from(services.values()) as MCPServiceConfig[];
}
/**
* Merge two configuration objects
* Deep merge with override behavior
*/
private mergeConfigs(base: any, override: any): any {
const merged = { ...base };
for (const key in override) {
if (override[key] === undefined) continue;
if (typeof override[key] === 'object' && !Array.isArray(override[key])) {
merged[key] = this.mergeConfigs(merged[key] || {}, override[key]);
} else {
merged[key] = override[key];
}
}
return merged;
}
/**
* Get the loaded configuration
* Returns validated configuration object
*/
getConfig(): HubConfig {
if (!this.config) {
throw new Error('Configuration not loaded');
}
return this.config;
}
/**
* Get enabled services
* Returns only services marked as enabled
*/
getEnabledServices(): MCPServiceConfig[] {
if (!this.config) {
throw new Error('Configuration not loaded');
}
return this.config.services.filter(s => s.enabled);
}
/**
* Get service configuration by ID
* Returns specific service configuration
*/
getService(serviceId: string): MCPServiceConfig | undefined {
if (!this.config) {
throw new Error('Configuration not loaded');
}
return this.config.services.find(s => s.id === serviceId);
}
/**
* Update service configuration
* Modifies service settings at runtime
*/
updateService(serviceId: string, updates: Partial<MCPServiceConfig>): void {
if (!this.config) {
throw new Error('Configuration not loaded');
}
const serviceIndex = this.config.services.findIndex(s => s.id === serviceId);
if (serviceIndex === -1) {
throw new Error(`Service ${serviceId} not found`);
}
this.config.services[serviceIndex] = {
...this.config.services[serviceIndex],
...updates
};
logger.info(`Updated service configuration: ${serviceId}`);
}
/**
* Save configuration to file
* Persists current configuration
*/
async saveConfig(): Promise<void> {
if (!this.config) {
throw new Error('Configuration not loaded');
}
try {
const configData = JSON.stringify(this.config, null, 2);
await fs.writeFile(this.configPath, configData, 'utf-8');
logger.info('Configuration saved to file', { path: this.configPath });
} catch (error) {
logger.error('Failed to save configuration:', error);
throw error;
}
}
/**
* Reload configuration from sources
* Refreshes configuration without restart
*/
async reloadConfig(): Promise<void> {
logger.info('Reloading configuration...');
this.config = null;
await this.loadConfig();
}
/**
* Validate service configuration
* Checks if service config is valid
*/
validateServiceConfig(service: MCPServiceConfig): boolean {
try {
// Basic validation
if (!service.id || !service.name || !service.transport) {
return false;
}
// Transport-specific validation
switch (service.transport) {
case 'stdio':
return !!service.command;
case 'websocket':
case 'http':
return !!service.url || !!service.endpoint;
default:
return false;
}
} catch {
return false;
}
}
}