config.tsโข5.86 kB
/**
* Configuration Loader
*
* This module:
* 1. Loads configuration from environment variables
* 2. Provides sensible defaults
* 3. Validates configuration against Zod schema
* 4. Provides type-safe access to configuration
*
* ๐ TEACHING: Why environment-only approach?
* - Simplicity: Single source of truth (environment variables)
* - Security: No config files with credentials
* - MCP-friendly: Works with Claude Desktop's env config
* - Validation: Catches configuration errors at startup
* - Type Safety: TypeScript knows the structure
*/
import { config as loadEnv } from 'dotenv';
import { ConfigSchema, type Config } from './config.schema.js';
/**
* Load .env file if it exists
*/
loadEnv();
/**
* Parse integer from environment variable with default
*/
function parseEnvInt(key: string, defaultValue: number): number {
const value = process.env[key];
if (!value) return defaultValue;
const parsed = parseInt(value, 10);
return isNaN(parsed) ? defaultValue : parsed;
}
/**
* Load and validate configuration from environment variables
*
* @returns Validated configuration object
* @throws Error if configuration is invalid or required values are missing
*/
export function loadConfig(): Config {
// Build config from environment variables
const rawConfig = {
server: {
name: process.env.MCP_SERVER_NAME || 'rs-waybill-mcp',
version: process.env.MCP_SERVER_VERSION || '1.0.0',
description: process.env.MCP_SERVER_DESCRIPTION || 'MCP server for RS.ge Waybill SOAP API',
},
api: {
baseUrl: process.env.API_BASE_URL || 'https://services.rs.ge/WayBillService/WayBillService.asmx',
timeout: parseEnvInt('API_TIMEOUT', 30000),
retries: parseEnvInt('API_RETRIES', 3),
retryDelay: parseEnvInt('API_RETRY_DELAY', 2000),
},
credentials: {
serviceUser: process.env.RS_SERVICE_USER,
servicePassword: process.env.RS_SERVICE_PASSWORD,
},
logging: {
level: (process.env.LOG_LEVEL as 'error' | 'warn' | 'info' | 'debug') || 'info',
file: process.env.LOG_FILE || 'logs/mcp-server.log',
console: false, // CRITICAL: Must be false for MCP - stdio is used for JSON protocol
maxSize: process.env.LOG_MAX_SIZE || '10m',
maxFiles: parseEnvInt('LOG_MAX_FILES', 7),
},
features: {
getWaybills: process.env.FEATURE_GET_WAYBILLS !== 'false',
saveWaybill: process.env.FEATURE_SAVE_WAYBILL !== 'false',
sendWaybill: process.env.FEATURE_SEND_WAYBILL !== 'false',
closeWaybill: process.env.FEATURE_CLOSE_WAYBILL !== 'false',
confirmWaybill: process.env.FEATURE_CONFIRM_WAYBILL !== 'false',
rejectWaybill: process.env.FEATURE_REJECT_WAYBILL !== 'false',
getErrorCodes: process.env.FEATURE_GET_ERROR_CODES !== 'false',
getAkcizCodes: process.env.FEATURE_GET_AKCIZ_CODES !== 'false',
getNameFromTin: process.env.FEATURE_GET_NAME_FROM_TIN !== 'false',
},
customization: {
dateFormat: process.env.DATE_FORMAT || 'YYYY-MM-DD',
soapNamespace: process.env.SOAP_NAMESPACE || 'http://tempuri.org/',
},
};
try {
// Validate with Zod schema
const validatedConfig = ConfigSchema.parse(rawConfig);
// Don't log to console - breaks MCP JSON protocol
return validatedConfig;
} catch (error) {
throw new Error(
`Configuration validation failed:\n${error}\n\n` +
`Please check your environment variables in .env file or Claude Desktop config`
);
}
}
/**
* Get credentials from config
*
* @throws Error if credentials are not set
*/
export function getCredentials(config: Config): { user: string; password: string } {
const user = config.credentials?.serviceUser;
const password = config.credentials?.servicePassword;
if (!user || !password) {
throw new Error(
'RS.ge credentials not configured.\n\n' +
'Please set RS_SERVICE_USER and RS_SERVICE_PASSWORD environment variables.\n\n' +
'Example (.env file):\n' +
' RS_SERVICE_USER=4053098841:405309884\n' +
' RS_SERVICE_PASSWORD=YourPasswordHere\n\n' +
'For Claude Desktop, add to claude_desktop_config.json:\n' +
' "env": {\n' +
' "RS_SERVICE_USER": "4053098841:405309884",\n' +
' "RS_SERVICE_PASSWORD": "YourPasswordHere"\n' +
' }\n\n' +
'See claude_desktop_config.example.json for full example.'
);
}
return { user, password };
}
/**
* Get logging configuration with defaults
*/
export function getLoggingConfig(config: Config): Required<Config['logging']> {
return {
level: config.logging?.level || 'info',
file: config.logging?.file || 'logs/mcp-server.log',
console: false, // CRITICAL: Always false for MCP servers - stdio is for JSON protocol only
maxSize: config.logging?.maxSize || '10m',
maxFiles: config.logging?.maxFiles || 7,
};
}
/**
* Get features configuration with defaults
*/
export function getFeaturesConfig(config: Config): Required<Config['features']> {
return {
getWaybills: config.features?.getWaybills ?? true,
saveWaybill: config.features?.saveWaybill ?? true,
sendWaybill: config.features?.sendWaybill ?? true,
closeWaybill: config.features?.closeWaybill ?? true,
confirmWaybill: config.features?.confirmWaybill ?? true,
rejectWaybill: config.features?.rejectWaybill ?? true,
getErrorCodes: config.features?.getErrorCodes ?? true,
getAkcizCodes: config.features?.getAkcizCodes ?? true,
getNameFromTin: config.features?.getNameFromTin ?? true,
};
}
/**
* Get customization configuration with defaults
*/
export function getCustomizationConfig(config: Config): Required<Config['customization']> {
return {
dateFormat: config.customization?.dateFormat || 'YYYY-MM-DD',
soapNamespace: config.customization?.soapNamespace || 'http://tempuri.org/',
};
}