import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { AnyAuthConfig, NoAuthConfig } from './auth/types.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Configuración general del servidor MCP
*/
export interface ServerConfig {
/** URL base de la API */
apiUrl: string;
/** Ruta al archivo OpenAPI spec (relativa al directorio del proyecto o absoluta) */
openApiSpecPath: string;
/** Configuración de autenticación */
auth: AnyAuthConfig;
/** Nombre del servidor MCP (opcional) */
serverName?: string;
/** Versión del servidor MCP (opcional) */
serverVersion?: string;
}
/**
* Configuración por defecto
*/
const defaultConfig: ServerConfig = {
apiUrl: 'http://localhost:8080/api',
openApiSpecPath: './open-api.json',
auth: { type: 'none' } as NoAuthConfig,
serverName: 'any-api-mcp',
serverVersion: '0.1.0'
};
/**
* Carga la configuración desde múltiples fuentes
* Prioridad: variables de entorno > archivo de config > defaults
*/
export function loadConfig(): ServerConfig {
let config: Partial<ServerConfig> = { ...defaultConfig };
// 1. Intentar cargar desde archivo de configuración
const configPaths = [
process.env.CONFIG_PATH,
path.resolve(__dirname, '../config.json'),
path.resolve(__dirname, '../config.local.json'),
path.resolve(process.cwd(), 'config.json'),
].filter(Boolean) as string[];
for (const configPath of configPaths) {
try {
if (fs.existsSync(configPath)) {
const fileContent = fs.readFileSync(configPath, 'utf-8');
const fileConfig = JSON.parse(fileContent);
config = deepMerge(config, fileConfig);
console.error(`[Config] Loaded configuration from ${configPath}`);
break;
}
} catch (error) {
console.error(`[Config] Error loading ${configPath}:`, error);
}
}
// 2. Sobrescribir con variables de entorno
if (process.env.API_URL) {
config.apiUrl = process.env.API_URL;
}
if (process.env.OPENAPI_SPEC_PATH) {
config.openApiSpecPath = process.env.OPENAPI_SPEC_PATH;
}
if (process.env.SERVER_NAME) {
config.serverName = process.env.SERVER_NAME;
}
// 3. Cargar auth desde variables de entorno si no hay archivo de config
// Esto permite compatibilidad hacia atrás con el sistema anterior
if (config.auth?.type === 'none' && process.env.API_KEY && process.env.API_SECRET) {
config.auth = {
type: 'bearer-endpoint',
endpoint: process.env.AUTH_ENDPOINT || '/auth/token',
method: 'POST',
body: {
key: '${API_KEY}',
secret: '${API_SECRET}'
},
tokenPath: process.env.AUTH_TOKEN_PATH || 'token'
};
console.error('[Config] Using bearer-endpoint auth from environment variables');
}
// Si hay AUTH_TYPE en env, construir config de auth
if (process.env.AUTH_TYPE) {
config.auth = buildAuthConfigFromEnv();
}
// Resolver rutas relativas
if (config.openApiSpecPath && !path.isAbsolute(config.openApiSpecPath)) {
config.openApiSpecPath = path.resolve(__dirname, '..', config.openApiSpecPath);
}
return config as ServerConfig;
}
/**
* Construye la configuración de auth desde variables de entorno
*/
function buildAuthConfigFromEnv(): AnyAuthConfig {
const authType = process.env.AUTH_TYPE;
switch (authType) {
case 'none':
return { type: 'none' };
case 'basic':
return {
type: 'basic',
username: process.env.AUTH_USERNAME || '',
password: process.env.AUTH_PASSWORD || ''
};
case 'api-key':
return {
type: 'api-key',
keyName: process.env.AUTH_KEY_NAME || 'X-API-Key',
keyValue: process.env.AUTH_KEY_VALUE || '',
location: (process.env.AUTH_KEY_LOCATION || 'header') as 'header' | 'query' | 'cookie',
prefix: process.env.AUTH_KEY_PREFIX
};
case 'bearer-token':
return {
type: 'bearer-token',
token: process.env.AUTH_TOKEN || ''
};
case 'bearer-endpoint':
return {
type: 'bearer-endpoint',
endpoint: process.env.AUTH_ENDPOINT || '/auth/token',
method: (process.env.AUTH_METHOD || 'POST') as 'GET' | 'POST',
body: process.env.AUTH_BODY ? JSON.parse(process.env.AUTH_BODY) : {
key: '${API_KEY}',
secret: '${API_SECRET}'
},
tokenPath: process.env.AUTH_TOKEN_PATH || 'token',
expiresIn: process.env.AUTH_EXPIRES_IN ? parseInt(process.env.AUTH_EXPIRES_IN) : undefined
};
case 'oauth2-client-credentials':
return {
type: 'oauth2-client-credentials',
tokenUrl: process.env.AUTH_TOKEN_URL || '',
clientId: process.env.AUTH_CLIENT_ID || '',
clientSecret: process.env.AUTH_CLIENT_SECRET || '',
scopes: process.env.AUTH_SCOPES ? process.env.AUTH_SCOPES.split(',') : undefined,
credentialsInBody: process.env.AUTH_CREDENTIALS_IN_BODY === 'true'
};
default:
console.error(`[Config] Unknown AUTH_TYPE: ${authType}, defaulting to 'none'`);
return { type: 'none' };
}
}
/**
* Deep merge de dos objetos
*/
function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
const result = { ...target };
for (const key of Object.keys(source) as Array<keyof T>) {
const sourceValue = source[key];
const targetValue = target[key];
if (sourceValue !== undefined) {
if (
typeof sourceValue === 'object' &&
sourceValue !== null &&
!Array.isArray(sourceValue) &&
typeof targetValue === 'object' &&
targetValue !== null &&
!Array.isArray(targetValue)
) {
result[key] = deepMerge(targetValue, sourceValue as any);
} else {
result[key] = sourceValue as T[keyof T];
}
}
}
return result;
}
/**
* Valida que la configuración sea correcta
*/
export function validateConfig(config: ServerConfig): string[] {
const errors: string[] = [];
if (!config.apiUrl) {
errors.push('apiUrl is required');
}
if (!config.openApiSpecPath) {
errors.push('openApiSpecPath is required');
} else if (!fs.existsSync(config.openApiSpecPath)) {
errors.push(`OpenAPI spec not found at: ${config.openApiSpecPath}`);
}
if (!config.auth) {
errors.push('auth configuration is required');
}
// Validaciones específicas por tipo de auth
if (config.auth) {
switch (config.auth.type) {
case 'basic':
if (!config.auth.username) errors.push('auth.username is required for basic auth');
if (!config.auth.password) errors.push('auth.password is required for basic auth');
break;
case 'api-key':
if (!config.auth.keyName) errors.push('auth.keyName is required for api-key auth');
if (!config.auth.keyValue) errors.push('auth.keyValue is required for api-key auth');
break;
case 'bearer-token':
if (!config.auth.token) errors.push('auth.token is required for bearer-token auth');
break;
case 'bearer-endpoint':
if (!config.auth.endpoint) errors.push('auth.endpoint is required for bearer-endpoint auth');
if (!config.auth.tokenPath) errors.push('auth.tokenPath is required for bearer-endpoint auth');
break;
case 'oauth2-client-credentials':
if (!config.auth.tokenUrl) errors.push('auth.tokenUrl is required for oauth2 auth');
if (!config.auth.clientId) errors.push('auth.clientId is required for oauth2 auth');
if (!config.auth.clientSecret) errors.push('auth.clientSecret is required for oauth2 auth');
break;
}
}
return errors;
}