Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note) avec activation guidée, tableau éditable, kanban et suppression de colonnes. Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN. Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la robustesse du serveur MCP (config, validation, rate-limit, métriques). Co-authored-by: Cursor <cursoragent@cursor.com>
362 lines
11 KiB
JavaScript
362 lines
11 KiB
JavaScript
/**
|
|
* Memento MCP Server - Configuration Management
|
|
*
|
|
* Centralized configuration with validation and defaults.
|
|
* Validates all required environment variables on startup.
|
|
*/
|
|
|
|
/**
|
|
* Parse boolean from string or value
|
|
*/
|
|
function parseBoolean(value, defaultValue = false) {
|
|
if (value === undefined || value === null || value === '') {
|
|
return defaultValue;
|
|
}
|
|
if (typeof value === 'boolean') return value;
|
|
const str = String(value).toLowerCase();
|
|
return ['true', '1', 'yes', 'on'].includes(str);
|
|
}
|
|
|
|
/**
|
|
* Parse integer with default
|
|
*/
|
|
function parseInt(value, defaultValue, min, max) {
|
|
if (value === undefined || value === null || value === '') {
|
|
return defaultValue;
|
|
}
|
|
const parsed = Number.parseInt(value, 10);
|
|
if (Number.isNaN(parsed)) return defaultValue;
|
|
if (min !== undefined && parsed < min) return min;
|
|
if (max !== undefined && parsed > max) return max;
|
|
return parsed;
|
|
}
|
|
|
|
/**
|
|
* Parse array from comma-separated string
|
|
*/
|
|
function parseArray(value, defaultValue = []) {
|
|
if (!value) return defaultValue;
|
|
if (Array.isArray(value)) return value;
|
|
return String(value).split(',').map((s) => s.trim()).filter(Boolean);
|
|
}
|
|
|
|
/**
|
|
* Get environment variable with fallback
|
|
*/
|
|
function env(key, fallback = '') {
|
|
return process.env[key] || fallback;
|
|
}
|
|
|
|
/**
|
|
* Validate database URL format
|
|
*/
|
|
function validateDatabaseUrl(url) {
|
|
if (!url) {
|
|
return { valid: false, error: 'DATABASE_URL is required' };
|
|
}
|
|
|
|
const isValid =
|
|
url.startsWith('postgresql://') ||
|
|
url.startsWith('postgres://') ||
|
|
url.startsWith('file:') ||
|
|
url.includes('.db') ||
|
|
url.includes('.sqlite');
|
|
|
|
if (!isValid) {
|
|
return {
|
|
valid: false,
|
|
error: 'DATABASE_URL must be a valid PostgreSQL or SQLite connection string',
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Validate port number
|
|
*/
|
|
function validatePort(port) {
|
|
if (port < 1 || port > 65535) {
|
|
return { valid: false, error: `PORT must be between 1 and 65535, got ${port}` };
|
|
}
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Validate log level
|
|
*/
|
|
function validateLogLevel(level) {
|
|
const validLevels = ['debug', 'info', 'warn', 'error', 'silent'];
|
|
if (!validLevels.includes(level)) {
|
|
return {
|
|
valid: false,
|
|
error: `LOG_LEVEL must be one of: ${validLevels.join(', ')}`,
|
|
};
|
|
}
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Validate timeout values
|
|
*/
|
|
function validateTimeout(timeout, name) {
|
|
const min = 1000; // 1 second minimum
|
|
const max = 300000; // 5 minutes maximum
|
|
|
|
if (timeout < min || timeout > max) {
|
|
return {
|
|
valid: false,
|
|
error: `${name} must be between ${min}ms and ${max}ms, got ${timeout}ms`,
|
|
};
|
|
}
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Configuration object with validation
|
|
*/
|
|
export const config = {
|
|
// Server
|
|
port: parseInt(env('PORT', '3001'), 3001, 1, 65535),
|
|
nodeEnv: env('NODE_ENV', 'development'),
|
|
|
|
// Database
|
|
databaseUrl: env('DATABASE_URL', ''),
|
|
isPostgres:
|
|
env('DATABASE_URL', '').startsWith('postgresql://') ||
|
|
env('DATABASE_URL', '').startsWith('postgres://'),
|
|
connectionLimit: parseInt(env('DB_CONNECTION_LIMIT', '10'), 10, 1, 100),
|
|
poolTimeout: parseInt(env('DB_POOL_TIMEOUT', '10'), 10, 1, 60),
|
|
|
|
// Application
|
|
appBaseUrl: env('APP_BASE_URL', 'http://localhost:3000'),
|
|
userId: env('USER_ID', null), // Optional user filter
|
|
|
|
// Authentication
|
|
requireAuth: parseBoolean(env('MCP_REQUIRE_AUTH'), false),
|
|
staticApiKey: env('MCP_API_KEY', null),
|
|
|
|
// Logging
|
|
logLevel: env('MCP_LOG_LEVEL', 'info').toLowerCase(),
|
|
logToFile: parseBoolean(env('MCP_LOG_TO_FILE'), false),
|
|
|
|
// Performance
|
|
requestTimeout: parseInt(env('MCP_REQUEST_TIMEOUT', '30000'), 30000, 1000, 300000),
|
|
rateLimit: parseInt(env('MCP_RATE_LIMIT', '100'), 100, 1, 10000),
|
|
rateLimitWindow: parseInt(env('MCP_RATE_LIMIT_WINDOW', '60000'), 60000, 1000, 3600000),
|
|
|
|
// Session management
|
|
maxSessions: parseInt(env('MCP_MAX_SESSIONS', '500'), 500, 10, 10000),
|
|
sessionTtl: parseInt(env('MCP_SESSION_TTL', '3600000'), 3600000, 60000, 86400000),
|
|
sessionCleanupInterval: parseInt(env('MCP_SESSION_CLEANUP_INTERVAL', '300000'), 300000, 60000, 3600000),
|
|
|
|
// Caching
|
|
enableCache: parseBoolean(env('MCP_ENABLE_CACHE'), true),
|
|
cacheTtl: parseInt(env('MCP_CACHE_TTL', '60000'), 60000, 0, 3600000),
|
|
cacheMaxSize: parseInt(env('MCP_CACHE_MAX_SIZE', '1000'), 1000, 100, 10000),
|
|
|
|
// Features
|
|
enableMetrics: parseBoolean(env('MCP_ENABLE_METRICS'), true),
|
|
enableAuditLog: parseBoolean(env('MCP_ENABLE_AUDIT_LOG'), true),
|
|
enableTools: parseArray(env('MCP_ENABLE_TOOLS'), null), // null = all tools enabled
|
|
disableTools: parseArray(env('MCP_DISABLE_TOOLS'), []),
|
|
|
|
// Security
|
|
maxRequestSize: parseInt(env('MCP_MAX_REQUEST_SIZE', '10485760'), 10485760, 1024, 104857600), // 10MB default
|
|
maxResponseSize: parseInt(env('MCP_MAX_RESPONSE_SIZE', '52428800'), 52428800, 1024, 524288000), // 50MB default
|
|
allowedOrigins: parseArray(env('MCP_ALLOWED_ORIGINS'), '*'),
|
|
|
|
// Observability
|
|
metricsPath: env('MCP_METRICS_PATH', '/metrics'),
|
|
healthPath: env('MCP_HEALTH_PATH', '/health'),
|
|
debugPath: env('MCP_DEBUG_PATH', '/debug'),
|
|
|
|
// Timeouts
|
|
databaseQueryTimeout: parseInt(env('MCP_DB_QUERY_TIMEOUT', '30000'), 30000, 1000, 120000),
|
|
toolExecutionTimeout: parseInt(env('MCP_TOOL_TIMEOUT', '60000'), 60000, 5000, 300000),
|
|
};
|
|
|
|
/**
|
|
* Validate all configuration values
|
|
* Returns array of validation errors (empty if valid)
|
|
*/
|
|
export function validateConfig() {
|
|
const errors = [];
|
|
|
|
// Required fields
|
|
if (!config.databaseUrl) {
|
|
errors.push({
|
|
key: 'DATABASE_URL',
|
|
message: 'DATABASE_URL is required',
|
|
critical: true,
|
|
});
|
|
} else {
|
|
const dbValidation = validateDatabaseUrl(config.databaseUrl);
|
|
if (!dbValidation.valid) {
|
|
errors.push({ key: 'DATABASE_URL', message: dbValidation.error, critical: true });
|
|
}
|
|
}
|
|
|
|
// Port validation
|
|
const portValidation = validatePort(config.port);
|
|
if (!portValidation.valid) {
|
|
errors.push({ key: 'PORT', message: portValidation.error, critical: true });
|
|
}
|
|
|
|
// Log level validation
|
|
const logLevelValidation = validateLogLevel(config.logLevel);
|
|
if (!logLevelValidation.valid) {
|
|
errors.push({ key: 'MCP_LOG_LEVEL', message: logLevelValidation.error, critical: false });
|
|
}
|
|
|
|
// Timeout validations
|
|
const requestTimeoutValidation = validateTimeout(config.requestTimeout, 'REQUEST_TIMEOUT');
|
|
if (!requestTimeoutValidation.valid) {
|
|
errors.push({
|
|
key: 'MCP_REQUEST_TIMEOUT',
|
|
message: requestTimeoutValidation.error,
|
|
critical: false,
|
|
});
|
|
}
|
|
|
|
const dbTimeoutValidation = validateTimeout(config.databaseQueryTimeout, 'DB_QUERY_TIMEOUT');
|
|
if (!dbTimeoutValidation.valid) {
|
|
errors.push({
|
|
key: 'MCP_DB_QUERY_TIMEOUT',
|
|
message: dbTimeoutValidation.error,
|
|
critical: false,
|
|
});
|
|
}
|
|
|
|
// Auth configuration
|
|
if (config.requireAuth && !config.staticApiKey) {
|
|
// Warning: auth required but no static key set
|
|
// This is OK - we'll use database-stored keys
|
|
// Just log a warning in development
|
|
if (config.nodeEnv === 'development') {
|
|
errors.push({
|
|
key: 'MCP_REQUIRE_AUTH',
|
|
message: 'Auth is required but no MCP_API_KEY is set. Database API keys will be used.',
|
|
critical: false,
|
|
level: 'warning',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for conflicting tool enable/disable
|
|
if (config.enableTools && config.disableTools.length > 0) {
|
|
const conflicts = config.enableTools.filter((t) => config.disableTools.includes(t));
|
|
if (conflicts.length > 0) {
|
|
errors.push({
|
|
key: 'MCP_ENABLE_TOOLS / MCP_DISABLE_TOOLS',
|
|
message: `Tools both enabled and disabled: ${conflicts.join(', ')}. Disabled takes precedence.`,
|
|
critical: false,
|
|
level: 'warning',
|
|
});
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Get configuration for display (sanitized)
|
|
* Removes sensitive values like API keys and database URLs
|
|
*/
|
|
export function getPublicConfig() {
|
|
return {
|
|
port: config.port,
|
|
nodeEnv: config.nodeEnv,
|
|
isPostgres: config.isPostgres,
|
|
appBaseUrl: config.appBaseUrl,
|
|
userId: config.userId || null,
|
|
requireAuth: config.requireAuth,
|
|
hasStaticKey: Boolean(config.staticApiKey),
|
|
logLevel: config.logLevel,
|
|
requestTimeout: config.requestTimeout,
|
|
rateLimit: config.rateLimit,
|
|
maxSessions: config.maxSessions,
|
|
sessionTtl: config.sessionTtl,
|
|
enableCache: config.enableCache,
|
|
cacheTtl: config.cacheTtl,
|
|
enableMetrics: config.enableMetrics,
|
|
enableAuditLog: config.enableAuditLog,
|
|
enabledToolCount: config.enableTools?.length || 'all',
|
|
disabledToolCount: config.disableTools.length,
|
|
maxRequestSize: config.maxRequestSize,
|
|
allowedOrigins: config.allowedOrigins,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get database URL for logging (sanitized)
|
|
*/
|
|
export function getSafeDatabaseUrl() {
|
|
if (!config.databaseUrl) return '<not set>';
|
|
|
|
try {
|
|
const url = new URL(config.databaseUrl);
|
|
// Mask password
|
|
if (url.password) {
|
|
url.password = '***';
|
|
}
|
|
return url.toString();
|
|
} catch {
|
|
// If not a valid URL, return partially masked version
|
|
const url = config.databaseUrl;
|
|
if (url.includes(':')) {
|
|
const parts = url.split('@');
|
|
if (parts.length > 1) {
|
|
return `***@${parts[1]}`;
|
|
}
|
|
}
|
|
return url.substring(0, 20) + '...';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Print configuration on startup
|
|
*/
|
|
export function printConfig() {
|
|
const errors = validateConfig();
|
|
|
|
console.log(`
|
|
╔═══════════════════════════════════════════════════════╗
|
|
║ Memento MCP Server Configuration ║
|
|
╚═══════════════════════════════════════════════════════╝
|
|
|
|
Environment: ${config.nodeEnv.toUpperCase()}
|
|
Port: ${config.port}
|
|
Database: ${config.isPostgres ? 'PostgreSQL' : 'SQLite'}
|
|
Database URL: ${getSafeDatabaseUrl()}
|
|
|
|
Authentication: ${config.requireAuth ? 'ENABLED' : 'DISABLED'}
|
|
Static Key: ${config.staticApiKey ? 'SET' : 'NOT SET'}
|
|
Rate Limit: ${config.rateLimit} requests / ${config.rateLimitWindow}ms
|
|
|
|
Sessions:
|
|
Max: ${config.maxSessions}
|
|
TTL: ${config.sessionTtl}ms
|
|
Cleanup: ${config.sessionCleanupInterval}ms
|
|
|
|
Timeouts:
|
|
Request: ${config.requestTimeout}ms
|
|
DB Query: ${config.databaseQueryTimeout}ms
|
|
Tool: ${config.toolExecutionTimeout}ms
|
|
|
|
Cache: ${config.enableCache ? 'ENABLED' : 'DISABLED'}
|
|
TTL: ${config.cacheTtl}ms
|
|
Max Size: ${config.cacheMaxSize}
|
|
|
|
Features:
|
|
Metrics: ${config.enableMetrics ? 'ENABLED' : 'DISABLED'}
|
|
Audit Log: ${config.enableAuditLog ? 'ENABLED' : 'DISABLED'}
|
|
|
|
${errors.length > 0 ? `⚠️ CONFIGURATION WARNINGS/ERRORS:
|
|
${errors.map((e) => ` ${e.critical ? '❌' : '⚠️'} ${e.key}: ${e.message}`).join('\n')}
|
|
` : ''}
|
|
`);
|
|
}
|
|
|
|
export default config;
|